Click here to Skip to main content
Click here to Skip to main content

Unicode compliant multilingual word breaker

, 21 Apr 2005
Rate this:
Please Sign up or sign in to vote.
A simple but effective multilingual word breaker for use with text retrieval systems.

Introduction

This article will show a method for breaking input Unicode text into separate words. It also will show an effective method of dealing with languages such as Chinese, Korean, Japanese etc. that do not have a concept of "words" without resorting to complex dictionary / linguistic analysis methods.

Finally some tips for using this in a real world application will be presented.

Background

I am currently working on a business application that must provide a way for users to quickly search for and retrieve information that they have previously entered. Because our application is fully internationalized and globalized, the document indexing and retrieval must support globalization as well.

In our application there are two tables in a SQL Server database, one contains a dictionary of unique words indexed to date and the other contains a cross reference between the word and the document it was originally indexed from. When a new record is entered, it is indexed immediately and the dictionary and index are updated immediately by the same stored procedure that saves the record. This means a user can search immediately at one workstation for something a user had just entered at another.

The input search term is broken apart into words using the same method as the original documents were broken to create the dictionary and index. A SQL query then looks for all documents that contain all the search terms and returns a list of matching documents so the user can open the original document.

I had originally considered the Indexing service built into SQL Server 2005 however it is very limited when working with multiple languages at once, and can not be indexed on the fly as in our application so has to schedule lengthy indexing operations which means users can not search for data just entered previously etc. I then considered using the Microsoft indexing service itself since it's built into all modern versions of Windows. However, it isn't ideal as it also involves some work to ensure it works with every specific language the user needs to support, and there were some questions as to the legality of using it in our own application despite parts of it being the Windows base service API and after a month of not hearing back from Microsoft Legal I have given up on that avenue. What I needed is something simple and effective enough to do the job.

The problem

On the surface it seems easy and you may have implemented this yourself when dealing with English language text: just go over the input text, split out the words by finding the boundaries between them (whitespace, punctuation etc.).

There are several problems with this approach when indexing non-English text:

  1. Some languages do not have a concept of a "word" hence no whitespace or punctuation exists to determine word boundaries (Chinese, Japanese, Korean, etc.).
  2. In many languages, one or more Unicode characters may make up what the user thinks of as a character or basic unit of the language.
  3. Some languages are entered from right to left and must be handled accordingly.

The problem seemed overwhelmingly complex and time consuming: Just the sort of thing that 3rd party components were invented for! However after doing some research, it appears that there is no solution that is small, flexible and not prohibitively expensive. Most solutions revolve around complex word stemming and dictionary matching methods which means that you have to force your users to install a language specific component of quite a large size. It also means that quite a significant investment needs to be made in every language you intend to sell in. This wouldn't work for our application because it is intended to be inexpensive and multi-lingual in a dynamic manner meaning more than one user can be using the same database in different languages at the same time.

I have written a solution that appears to do the job. In the ideal world, it would be perfect, but this world is not ideal so any feedback and suggestions would be gratefully appreciated.

The solution

I won't show any examples of the WordBreaker class source code here because the code is simple, straightforward and well commented, however the techniques are interesting and useful to know:

The right to left issue and the issue with more than one Unicode character being used to represent what the user sees as a single character are both easily solved by the TextElementEnumerator in the System.Globalization class. This handy class will properly iterate through the source text returning each text element. It returns text elements in the correct order depending upon whether the source text is right to left or left to right and it understands the concept of a series of Unicode characters representing a single visual grapheme character as the reader sees it.

The most insurmountable obstacle to making our own solution appeared to be the languages without word boundaries, however after doing some research on the subject, it appears that there is a solution called n-gram or overlapping indexing that is in use and works effectively.

The premise is that since you can't identify the boundaries between Chinese "words" (for example), what you can do is index every second character in an overlapping scheme. For example: if the input text is "C1C2C3C4" (c1 being one Chinese character, c2 another etc.) you break it apart as: "C1C2", "C2C3","C3C4" etc. This works because when the user enters their search term you break it apart similarly. The downside is of course that your index of words is going to be much larger, however you do not need to provide a dictionary and word stemming / linguistic analysis, so in the end the tradeoff may not be bad at all.

There is a good paper on the Microsoft Research website (a lot of good stuff there): "On the use of Words and N-grams for Chinese Information Retrieval" on the n-gram method I use as well as the more advanced methods. The paper indicates that the n-gram method I'm using is as effective as other methods with the downside of the larger time it takes to index (they are basing their paper on an app that indexes all the documents all at once), however since in our app we index on the fly during a save or update, the time delay is negligible. The other downside is of course the size of the dictionary being much larger than indexing Latin text, however in our app there are typically not enough documents with unique words to make much difference either way.

This n-gram breaking is simply accomplished by scrolling a moving "window" over the text in question. The only tricky parts are spotting a boundary between Latin text and CJK text -- it is common for CJK text to contain Latin text within it and we want to ensure that we break apart the Latin text using the more appropriate process of spotting word boundaries. This is solved by looking for characters in the sub 256 decimal Unicode range. This ensures that we catch not only standard Latin characters but extended Latin as well to cover languages other than English that use the extended Latin-1 Unicode block (Spanish, French, Croatian etc.) for Latin characters with Acute, Circumflex, Grave etc. accents.

Using the code

The word breaker is implemented as a static function Break in the class WordBreaker:

public static string Break(bool breakStyleCJK, 
           bool returnAsXml, params string[] text)

Parameters

  • breakStyleCJK - uses rolling overlap 2 n-gram method of breaking the text. If Latin characters are encountered they will be broken using punctuation and spacing as full words.
  • returnAsXml - returns the resulting list of unique words as a fragment of XML suitable for passing to a SQL Server stored procedure or whatever you have. If false it will return words as comma delimited strings.
  • text - a series of 1 to * strings to be broken.
  • Returns - a string containing either a fragment of XML or a series of comma delimited strings.

If the return as XML method is chosen the XML looks like this:

<Items>
  <i w="hello" />
  <i w="break" />
  <i w="text" />
</Items>

Real world implementation

I thought it might be helpful to include some bits and pieces that tie this into a real world application. As stated previously, we use a SQL Server database for persistent storage.

There are two tables used for the searching and indexing: a dictionary table and a key table. The dictionary table contains two columns, one for each word indexed with a unique constraint on it so that no word will be stored more than once and a second column containing a unique ID number for that word.

The index table is the cross reference between the dictionary and the documents that are indexed. In our case, it contains three fields, one for the word ID, one for the source object ID and a third to indicate the type of source object for easy retrieval later on.

Here is an example of a stored procedure that can take the XML generated by the word breaker and insert those words into a dictionary / index, it is called by the stored procedure that saves / updates a business object and the business object level code generates the keyword XML by passing all string fields to the word breaker and then passing on the result of the wordbreak as a ntext parameter. The business object saving / updating stored procedure in turn calls this procedure passing off the keyword XML and the info that uniquely identifies the source object:

/*
Created XXXXXX
Coded by: John
Object: _ProcessKeywords utility sp.
Purpose: Storing keywords for search
Last update: XXXXX

Takes as input an XML fragment formatted like this:

<Items>
  <i w="firstword" />
  <i w="secondword" />
  <i w="etcetc" />
</Items>

Extracts the words and inserts them into the search dictionary table
A constraint on the SearchDictionary table ensures only new words are added

Then the RootObject ID and Type along with the words dictionary ID value
are inserted into the SEarchKey table for later retrieval during a search

RootobjectID and type are used to uniquely identify the source document.
*/
ALTER PROCEDURE dbo._ProcessKeywords

    (
        @KWXML ntext,
        @RootObjectID uniqueidentifier,
        @RootObjectType smallint,
        @ClearExistingIndex smallint = 0
    )

AS

    SET NOCOUNT ON
    IF DATALENGTH(@KWXML)<1 
        RETURN
    --Clear out any previous indexes for this object id and type in the 
    --SearchKey table
    --This ensures any words that no longer appear in the source document 
    --are not left hanging around in an old index of that document
    IF @ClearExistingIndex = 1
        DELETE FROM dbo._SearchKey WHERE (_SourceObjectID=@RootObjectID AND
                         _SourceObjectType=@RootObjectType)
    
        
     DECLARE @hDoc int --Handle to xml parser document
     DECLARE @word nvarchar(255) --Max 255 characters are indexed
     DECLARE @_srchid uniqueidentifier
     
     EXEC dbo.sp_xml_preparedocument @hDoc output, @KWXML
     
     --Declare a cursor that will be used to loop through the
     --wordlist xml items one by one
     DECLARE wordlist CURSOR
     FOR SELECT * FROM OPENXML(@hDoc,'/Items/i',1) WITH (w nvarchar(255))
     FOR READ ONLY
     
     OPEN wordlist
     
     FETCH wordlist into @word
     
     --Loop through the XML word list
     while(@@FETCH_STATUS=0) 
     BEGIN
        --Insert the word into the dictionary
        INSERT dbo._SearchDictionary (Word) values (@word)    
    
        --Get the word's ID value from the dictionary table
        SET @_srchid = (SELECT ID FROM dbo._SearchDictionary 
                        WHERE (Word = @word))
    
        --Update the search key table with the source object id and word id 
        INSERT dbo._SearchKey (_WordID, _SourceObjectID, _SourceObjectType)
            VALUES (@_srchid, @RootObjectID, @RootObjectType)
     
        --Get the next word    
        FETCH wordlist into @word
     END
     
     --Cleanup cursor
     CLOSE wordlist
     DEALLOCATE wordlist     
     
     EXEC dbo.sp_xml_removedocument @hDoc

This gives us a dictionary full of words and an index. To search for documents, the user's search term is broken using the exact same method as the original document, then it's passed to a query used to find all documents that contain all the keywords (there are other ways of doing this, but for our purposes we only want to return matches that contain all words).

Let's say the user is searching for the phrase "almond pear avocado", that query would look like this:

SELECT dbo._SearchKey._SourceObjectID, dbo._SearchKey._SourceObjectType
FROM   dbo._SearchDictionary INNER JOIN
       dbo._SearchKey ON dbo._SearchDictionary.ID = dbo._SearchKey._WordID
WHERE  (dbo._SearchDictionary.Word = N'almond') OR
       (dbo._SearchDictionary.Word = N'pear') OR
       (dbo._SearchDictionary.Word = N'avocado')
GROUP BY dbo._SearchKey._SourceObjectID, dbo._SearchKey._SourceObjectType
HAVING (COUNT(*) = 3)

Since the query is grouped by the source object's ID and type and OR is used with the keywords and we know that each keyword is indexed only once per source document, it will return a single result for each document that contains at least one occurrence of each of the keywords searched for. In this case there are three keywords, so there should be three results in each grouping of each document.

The resulting list of ID numbers can then be used to extract an excerpt to display to the user (coming in another article).

Remarks

Hopefully this will be of some use to others and I would appreciate any feedback towards improving it.

History

  • 21st April, 2005: initially posted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Member 96

Canada Canada
No Biography provided

Comments and Discussions

 
QuestionHow to determine language PinmemberScott Elder26-May-10 7:19 
AnswerRe: How to determine language PinmemberJohn C26-May-10 7:51 
GeneralMy vote of 10 Pinmemberdanielm928-Jan-10 3:25 
GeneralRe: My vote of 10 PinmemberJohn C8-Jan-10 5:37 
GeneralMy vote of 1 PinmemberHoward Richards29-Jan-09 22:10 
GeneralRe: My vote of 1 PinmemberDavid Pritchard21-Sep-09 0:55 
GeneralRe: My vote of 1 PinmemberJohn C21-Sep-09 4:58 
GeneralThe 'word's you got in Chinese seemed to be not very meaningful to me. Pinmemberzwu_ca18-Apr-08 13:33 
GeneralRe: The 'word's you got in Chinese seemed to be not very meaningful to me. PinmemberJohn C18-Apr-08 15:28 
GeneralThai language Pinmembermenn30006-Dec-07 19:29 
GeneralRe: Thai language PinmemberJohn C18-Apr-08 15:31 
Questionhow to convert english words into japanese Pinmemberdoddamani praveen25-Jul-06 5:09 
AnswerRe: how to convert english words into japanese PinmemberJohn Cardinal25-Jul-06 8:53 
GeneralRe: how to convert english words into japanese PinmemberPaulC197225-Jul-06 9:21 
GeneralRe: how to convert english words into japanese Pinmemberreinux6-Jan-08 11:26 
GeneralHelp Pinmembersreejith ss nair18-Sep-05 21:33 
GeneralRe: Help PinmemberJohn Cardinal19-Sep-05 4:58 
sreejith ss nair wrote:
1) I am not sure how to localize stored data in database.
2) How will I give or from where i generate localized information for all supported language.

 
I'm not sure I understand either of your questions, but I'll take a shot at it.
 
Data stored in the database would be stored as unicode if it's text so that takes care of that. For date and time values you would typically take them from the User Interface and convert them from the users locale to UTC values and store them in the database as UTC / GMT times, later converting them back to the users locale for display. That way no matter where the users is they see the correct adjusted date and time for their locale.
 
For currency values you are out of luck since there is no set conversion from one to another as there is with time zones it means that you will need to keep track of the currency being used by the person inputing data, and store that as well as the value, or alternatively you will need to choose a single currency and attempt some sort of conversion at the time of entry from the users local currency to the chosen single currency. Obviously there is a lot more to the currency aspect which is not worth going into because it depends on your application and you in any case must figure that out for yourself.
 

For your question 2 I believe you are asking about localizing your application into other languages. I have no answer for you other than google can bring up a lot of companies that do professional translation and you must budget for that as it will be very expensive to have it done for you.
 


"A preoccupation with the next world pretty clearly signals an inability to cope credibly with this one."
GeneralRe: Help Pinmembersreejith ss nair20-Sep-05 19:30 
GeneralRe: Help PinmemberJohn Cardinal21-Sep-05 5:13 
GeneralJapanese Pinmemberreinux26-Apr-05 10:33 
GeneralRe: Japanese PinmemberJohn Cardinal26-Apr-05 14:50 
GeneralRe: Japanese Pinmemberreinux26-Apr-05 22:10 
GeneralRe: Japanese Pinmembersteblublu6-Jan-08 10:42 
GeneralRe: Japanese Pinmemberreinux6-Jan-08 11:57 
GeneralRe: Japanese Pinmembersteblublu7-Jan-08 5:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 21 Apr 2005
Article Copyright 2005 by Member 96
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid