1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-24 22:22:08 +03:00

Change the fts3 snippet function to return (hopefully) more relevant snippets in less time.

FossilOrigin-Name: 8a208223a74d451f60d9cd707d63fb7d157d1737
This commit is contained in:
dan
2010-01-06 17:19:21 +00:00
parent cf3e518506
commit b023b04fcb
9 changed files with 642 additions and 920 deletions

View File

@ -1709,6 +1709,74 @@ static int fts3PhraseSelect(
return rc;
}
static int fts3NearMerge(
int mergetype, /* MERGE_POS_NEAR or MERGE_NEAR */
int nNear, /* Parameter to NEAR operator */
int nTokenLeft, /* Number of tokens in LHS phrase arg */
char *aLeft, /* Doclist for LHS (incl. positions) */
int nLeft, /* Size of LHS doclist in bytes */
int nTokenRight, /* As nTokenLeft */
char *aRight, /* As aLeft */
int nRight, /* As nRight */
char **paOut, /* OUT: Results of merge (malloced) */
int *pnOut /* OUT: Sized of output buffer */
){
char *aOut;
int rc;
assert( mergetype==MERGE_POS_NEAR || MERGE_NEAR );
aOut = sqlite3_malloc(nLeft+nRight+1);
if( aOut==0 ){
rc = SQLITE_NOMEM;
}else{
rc = fts3DoclistMerge(mergetype, nNear+nTokenRight, nNear+nTokenLeft,
aOut, pnOut, aLeft, nLeft, aRight, nRight
);
if( rc!=SQLITE_OK ){
sqlite3_free(aOut);
aOut = 0;
}
}
*paOut = aOut;
return rc;
}
int sqlite3Fts3ExprNearTrim(Fts3Expr *pLeft, Fts3Expr *pRight, int nNear){
int rc;
if( pLeft->aDoclist==0 || pRight->aDoclist==0 ){
sqlite3_free(pLeft->aDoclist);
sqlite3_free(pRight->aDoclist);
pRight->aDoclist = 0;
pLeft->aDoclist = 0;
rc = SQLITE_OK;
}else{
char *aOut;
int nOut;
rc = fts3NearMerge(MERGE_POS_NEAR, nNear,
pLeft->pPhrase->nToken, pLeft->aDoclist, pLeft->nDoclist,
pRight->pPhrase->nToken, pRight->aDoclist, pRight->nDoclist,
&aOut, &nOut
);
if( rc!=SQLITE_OK ) return rc;
sqlite3_free(pRight->aDoclist);
pRight->aDoclist = aOut;
pRight->nDoclist = nOut;
rc = fts3NearMerge(MERGE_POS_NEAR, nNear,
pRight->pPhrase->nToken, pRight->aDoclist, pRight->nDoclist,
pLeft->pPhrase->nToken, pLeft->aDoclist, pLeft->nDoclist,
&aOut, &nOut
);
sqlite3_free(pLeft->aDoclist);
pLeft->aDoclist = aOut;
pLeft->nDoclist = nOut;
}
return rc;
}
/*
** Evaluate the full-text expression pExpr against fts3 table pTab. Store
** the resulting doclist in *paOut and *pnOut.
@ -1753,9 +1821,6 @@ static int evalFts3Expr(
Fts3Expr *pLeft;
Fts3Expr *pRight;
int mergetype = isReqPos ? MERGE_POS_NEAR : MERGE_NEAR;
int nParam1;
int nParam2;
char *aBuffer;
if( pExpr->pParent && pExpr->pParent->eType==FTSQUERY_NEAR ){
mergetype = MERGE_POS_NEAR;
@ -1768,17 +1833,11 @@ static int evalFts3Expr(
assert( pRight->eType==FTSQUERY_PHRASE );
assert( pLeft->eType==FTSQUERY_PHRASE );
nParam1 = pExpr->nNear+1;
nParam2 = nParam1+pLeft->pPhrase->nToken+pRight->pPhrase->nToken-2;
aBuffer = sqlite3_malloc(nLeft+nRight+1);
rc = fts3DoclistMerge(mergetype, nParam1, nParam2, aBuffer,
pnOut, aLeft, nLeft, aRight, nRight
rc = fts3NearMerge(mergetype, pExpr->nNear,
pLeft->pPhrase->nToken, aLeft, nLeft,
pRight->pPhrase->nToken, aRight, nRight,
paOut, pnOut
);
if( rc!=SQLITE_OK ){
sqlite3_free(aBuffer);
}else{
*paOut = aBuffer;
}
sqlite3_free(aLeft);
break;
}
@ -2064,7 +2123,7 @@ char *sqlite3Fts3FindPositions(
pCsr++;
pCsr += sqlite3Fts3GetVarint32(pCsr, &iThis);
}
if( iCol==iThis ) return pCsr;
if( iCol==iThis && (*pCsr&0xFE) ) return pCsr;
}
return 0;
}
@ -2116,45 +2175,8 @@ static void fts3SnippetFunc(
const char *zStart = "<b>";
const char *zEnd = "</b>";
const char *zEllipsis = "<b>...</b>";
/* There must be at least one argument passed to this function (otherwise
** the non-overloaded version would have been called instead of this one).
*/
assert( nVal>=1 );
if( nVal>4 ){
sqlite3_result_error(pContext,
"wrong number of arguments to function snippet()", -1);
return;
}
if( fts3FunctionArg(pContext, "snippet", apVal[0], &pCsr) ) return;
switch( nVal ){
case 4: zEllipsis = (const char*)sqlite3_value_text(apVal[3]);
case 3: zEnd = (const char*)sqlite3_value_text(apVal[2]);
case 2: zStart = (const char*)sqlite3_value_text(apVal[1]);
}
if( !zEllipsis || !zEnd || !zStart ){
sqlite3_result_error_nomem(pContext);
}else if( SQLITE_OK==fts3CursorSeek(pContext, pCsr) ){
sqlite3Fts3Snippet(pContext, pCsr, zStart, zEnd, zEllipsis);
}
}
/*
** Implementation of the snippet2() function for FTS3
*/
static void fts3Snippet2Func(
sqlite3_context *pContext, /* SQLite function call context */
int nVal, /* Size of apVal[] array */
sqlite3_value **apVal /* Array of arguments */
){
Fts3Cursor *pCsr; /* Cursor handle passed through apVal[0] */
const char *zStart = "<b>";
const char *zEnd = "</b>";
const char *zEllipsis = "<b>...</b>";
int iCol = -1;
int nToken = 10;
int nToken = 15;
/* There must be at least one argument passed to this function (otherwise
** the non-overloaded version would have been called instead of this one).
@ -2178,7 +2200,7 @@ static void fts3Snippet2Func(
if( !zEllipsis || !zEnd || !zStart ){
sqlite3_result_error_nomem(pContext);
}else if( SQLITE_OK==fts3CursorSeek(pContext, pCsr) ){
sqlite3Fts3Snippet2(pContext, pCsr, zStart, zEnd, zEllipsis, iCol, nToken);
sqlite3Fts3Snippet(pContext, pCsr, zStart, zEnd, zEllipsis, iCol, nToken);
}
}
@ -2279,7 +2301,6 @@ static int fts3FindFunctionMethod(
void (*xFunc)(sqlite3_context*,int,sqlite3_value**);
} aOverload[] = {
{ "snippet", fts3SnippetFunc },
{ "snippet2", fts3Snippet2Func },
{ "offsets", fts3OffsetsFunc },
{ "optimize", fts3OptimizeFunc },
{ "matchinfo", fts3MatchinfoFunc },
@ -2429,7 +2450,6 @@ int sqlite3Fts3Init(sqlite3 *db){
if( SQLITE_OK==rc
&& SQLITE_OK==(rc = sqlite3Fts3InitHashTable(db, pHash, "fts3_tokenizer"))
&& SQLITE_OK==(rc = sqlite3_overload_function(db, "snippet", -1))
&& SQLITE_OK==(rc = sqlite3_overload_function(db, "snippet2", -1))
&& SQLITE_OK==(rc = sqlite3_overload_function(db, "offsets", 1))
&& SQLITE_OK==(rc = sqlite3_overload_function(db, "matchinfo", -1))
&& SQLITE_OK==(rc = sqlite3_overload_function(db, "optimize", 1))

View File

@ -279,6 +279,7 @@ void sqlite3Fts3Dequote(char *);
char *sqlite3Fts3FindPositions(Fts3Expr *, sqlite3_int64, int);
int sqlite3Fts3ExprLoadDoclist(Fts3Table *, Fts3Expr *);
int sqlite3Fts3ExprNearTrim(Fts3Expr *, Fts3Expr *, int);
/* fts3_tokenizer.c */
const char *sqlite3Fts3NextToken(const char *, int *);
@ -289,10 +290,7 @@ int sqlite3Fts3InitTokenizer(Fts3Hash *pHash,
/* fts3_snippet.c */
void sqlite3Fts3Offsets(sqlite3_context*, Fts3Cursor*);
void sqlite3Fts3Snippet(sqlite3_context*, Fts3Cursor*,
const char *, const char *, const char *
);
void sqlite3Fts3Snippet2(sqlite3_context *, Fts3Cursor *, const char *,
void sqlite3Fts3Snippet(sqlite3_context *, Fts3Cursor *, const char *,
const char *, const char *, int, int
);
void sqlite3Fts3Matchinfo(sqlite3_context *, Fts3Cursor *);

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,5 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
C Fix\san\sissue\swith\slemon\sgenerating\sincorrect\sgrammars.\s\sThis\sissue\sdoes\nnot\seffect\sSQLite.
D 2010-01-06T13:07:31
C Change\sthe\sfts3\ssnippet\sfunction\sto\sreturn\s(hopefully)\smore\srelevant\ssnippets\sin\sless\stime.
D 2010-01-06T17:19:22
F Makefile.arm-wince-mingw32ce-gcc fcd5e9cd67fe88836360bb4f9ef4cb7f8e2fb5a0
F Makefile.in c5827ead754ab32b9585487177c93bb00b9497b3
F Makefile.linux-gcc d53183f4aa6a9192d249731c90dbdffbd2c68654
@ -59,15 +56,15 @@ F ext/fts2/mkfts2amal.tcl 974d5d438cb3f7c4a652639262f82418c1e4cff0
F ext/fts3/README.syntax a19711dc5458c20734b8e485e75fb1981ec2427a
F ext/fts3/README.tokenizers 998756696647400de63d5ba60e9655036cb966e9
F ext/fts3/README.txt 8c18f41574404623b76917b9da66fcb0ab38328d
F ext/fts3/fts3.c 15fb87c1f00dfd88c2fbbbd9e50f319ea77834f0
F ext/fts3/fts3.c 04e95afa45789d7a3da59f458d4a8c1879c31446
F ext/fts3/fts3.h 3a10a0af180d502cecc50df77b1b22df142817fe
F ext/fts3/fts3Int.h 9326800fa10e06d8e9d6d519f873b1371252968a
F ext/fts3/fts3Int.h 45bc7e284806042119722c8f4127ee944b77f0dd
F ext/fts3/fts3_expr.c f4ff02ebe854e97ac03ff00b38b728a9ab57fd4b
F ext/fts3/fts3_hash.c 3c8f6387a4a7f5305588b203fa7c887d753e1f1c
F ext/fts3/fts3_hash.h 8331fb2206c609f9fc4c4735b9ab5ad6137c88ec
F ext/fts3/fts3_icu.c ac494aed69835008185299315403044664bda295
F ext/fts3/fts3_porter.c a651e287e02b49b565a6ccf9441959d434489156
F ext/fts3/fts3_snippet.c 0e38f76c5992dd08d20fc81e1265763370f9ea4f
F ext/fts3/fts3_snippet.c 612b3ad63abf2c5c85b6a46aac94bd90280e905a
F ext/fts3/fts3_tokenizer.c 1a49ee3d79cbf0b9386250370d9cbfe4bb89c8ff
F ext/fts3/fts3_tokenizer.h 13ffd9fcb397fec32a05ef5cd9e0fa659bf3dbd3
F ext/fts3/fts3_tokenizer1.c 11a604a53cff5e8c28882727bf794e5252e5227b
@ -383,7 +380,7 @@ F test/fts3.test ae0433b09b12def08105640e57693726c4949338
F test/fts3_common.tcl 2a2044688ce3addb1dd58d3d846c574cf4b7bbcd
F test/fts3aa.test 5327d4c1d9b6c61021696746cc9a6cdc5bf159c0
F test/fts3ab.test 09aeaa162aee6513d9ff336b6932211008b9d1f9
F test/fts3ac.test 356280144a2c92aa7b11474afadfe62a437fcd69
F test/fts3ac.test fc1ac42c33f8a66d48ae41e4728f7ca4b6dfc950
F test/fts3ad.test e40570cb6f74f059129ad48bcef3d7cbc20dda49
F test/fts3ae.test ce32a13b34b0260928e4213b4481acf801533bda
F test/fts3af.test d394978c534eabf22dd0837e718b913fd66b499c
@ -392,7 +389,7 @@ F test/fts3ah.test ba181d6a3dee0c929f0d69df67cac9c47cda6bff
F test/fts3ai.test d29cee6ed653e30de478066881cec8aa766531b2
F test/fts3aj.test 584facbc9ac4381a7ec624bfde677340ffc2a5a4
F test/fts3ak.test bd14deafe9d1586e8e9bf032411026ac4f8c925d
F test/fts3al.test 6d19619402d2133773262652fc3f185cdf6be667
F test/fts3al.test 07d64326e79bbdbab20ee87fc3328fbf01641c9f
F test/fts3am.test 218aa6ba0dfc50c7c16b2022aac5c6be593d08d8
F test/fts3an.test 931fa21bd80641ca594bfa32e105250a8a07918b
F test/fts3ao.test 0aa29dd4fc1c8d46b1f7cfe5926f7ac97551bea9
@ -405,9 +402,10 @@ F test/fts3e.test 1f6c6ac9cc8b772ca256e6b22aaeed50c9350851
F test/fts3expr.test 05dab77387801e4900009917bb18f556037d82da
F test/fts3expr2.test 18da930352e5693eaa163a3eacf96233b7290d1a
F test/fts3malloc.test d02ee86b21edd2b43044e0d6dfdcd26cb6efddcb
F test/fts3near.test dc196dd17b4606f440c580d45b3d23aa975fd077
F test/fts3near.test 2e318ee434d32babd27c167142e2b94ddbab4844
F test/fts3query.test ca21717993f51caa7e36231dba2499868f3f8a6f
F test/fts3rnd.test 153b4214bad6084a348814f3dd651a92e2f31d9b
F test/fts3snippet.test bfbceb2e292ddfdc6bb0b1b252ccea78bd6091be
F test/func.test af106ed834001738246d276659406823e35cde7b
F test/func2.test 772d66227e4e6684b86053302e2d74a2500e1e0f
F test/fuzz.test a4174c3009a3e2c2e14b31b364ebf7ddb49de2c9
@ -786,14 +784,7 @@ F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff
F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224
F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e
F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f
P 28d0d7710761114a44a1a3a425a6883c661f06e7
R cb14a38f5906a10fa21936447376b66a
U drh
Z 437ad8ccf6b703d6a3f5435217fbeff7
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.6 (GNU/Linux)
iD8DBQFLRIsWoxKgR168RlERAko3AJ9cRW4W+hFzWCSEF5rdeL83LKknrgCfQKRR
l/RSoin5yCY/+/3Q1I6oeNA=
=d16B
-----END PGP SIGNATURE-----
P 077a6bee2dd4668a5b13c37aa7d4c052350ec782
R 63513c05ce3003328b753382175b1505
U dan
Z 7955c05e9b09116e00ebafe15af16394

View File

@ -1 +1 @@
077a6bee2dd4668a5b13c37aa7d4c052350ec782
8a208223a74d451f60d9cd707d63fb7d157d1737

View File

@ -1131,39 +1131,36 @@ do_test fts3ac-4.2 {
SELECT snippet(email) FROM email
WHERE email MATCH 'christmas candlelight'
}
} {{<b>...</b> place.? What do you think about going here <b>Christmas</b>
eve?? They have an 11:00 a.m. service and a <b>candlelight</b> service at 5:00 p.m.,
among others. <b>...</b>}}
} {{<b>...</b>here <b>Christmas</b>
eve?? They have an 11:00 a.m. service and a <b>candlelight</b> service<b>...</b>}}
do_test fts3ac-4.3 {
execsql {
SELECT snippet(email) FROM email
WHERE email MATCH 'deal sheet potential reuse'
}
} {{EOL-Accenture <b>Deal</b> <b>Sheet</b> <b>...</b> intent
Review Enron asset base for <b>potential</b> <b>reuse</b>/ licensing
Contract negotiations <b>...</b>}}
} {{EOL-Accenture <b>Deal</b> <b>Sheet</b><b>...</b>asset base for <b>potential</b> <b>reuse</b>/ licensing
Contract negotiations<b>...</b>}}
do_test fts3ac-4.4 {
execsql {
SELECT snippet(email,'<<<','>>>',' ') FROM email
WHERE email MATCH 'deal sheet potential reuse'
}
} {{EOL-Accenture <<<Deal>>> <<<Sheet>>> intent
Review Enron asset base for <<<potential>>> <<<reuse>>>/ licensing
Contract negotiations }}
} {{EOL-Accenture <<<Deal>>> <<<Sheet>>> asset base for <<<potential>>> <<<reuse>>>/ licensing
Contract negotiations }}
do_test fts3ac-4.5 {
execsql {
SELECT snippet(email,'<<<','>>>',' ') FROM email
WHERE email MATCH 'first things'
}
} {{Re: <<<First>>> Polish Deal! Congrats! <<<Things>>> seem to be building rapidly now on the }}
} {{Re: <<<First>>> Polish Deal! Congrats! <<<Things>>> seem to be building rapidly now }}
do_test fts3ac-4.6 {
execsql {
SELECT snippet(email) FROM email
WHERE email MATCH 'chris is here'
}
} {{<b>chris</b>.germany@enron.com <b>...</b> Sounds good to me. I bet this <b>is</b> next to the Warick?? Hotel. <b>...</b> place.? What do you think about going <b>here</b> Christmas
eve?? They have an 11:00 a.m. <b>...</b>}}
} {{<b>...</b><b>chris</b>.germany@enron.com'" <<b>chris</b><b>...</b>bet this <b>is</b> next to<b>...</b>about going <b>here</b> Christmas
eve<b>...</b>}}
do_test fts3ac-4.7 {
execsql {
SELECT snippet(email) FROM email
@ -1171,19 +1168,15 @@ do_test fts3ac-4.7 {
}
} {{Erin:
<b>Pursuant</b> <b>to</b> your request, attached are the Schedule to <b>...</b>}}
<b>Pursuant</b> <b>to</b> your request, attached are the Schedule to the ISDA Master Agreement, together<b>...</b>}}
do_test fts3ac-4.8 {
execsql {
SELECT snippet(email) FROM email
WHERE email MATCH 'ancillary load davis'
}
} {{pete.<b>davis</b>@enron.com <b>...</b> Start Date: 4/22/01; HourAhead hour: 3; No <b>ancillary</b> schedules awarded.
Variances detected.
Variances detected in <b>Load</b> schedule.
} {{pete.<b>davis</b>@enron.com<b>...</b>3; No <b>ancillary</b> schedules awarded<b>...</b>detected in <b>Load</b> schedule.
LOG MESSAGES:
PARSING <b>...</b>}}
LOG<b>...</b>}}
# Combinations of AND and OR operators:
#
@ -1192,22 +1185,17 @@ do_test fts3ac-5.1 {
SELECT snippet(email) FROM email
WHERE email MATCH 'questar enron OR com'
}
} {{matt.smith@<b>enron</b>.<b>com</b> <b>...</b> six reports:
31 Keystone Receipts
} {{matt.smith@<b>enron</b>.<b>com</b><b>...</b>31 Keystone Receipts
15 <b>Questar</b> Pipeline
40 Rockies Production
22 West_2 <b>...</b>}}
40 Rockies<b>...</b>}}
do_test fts3ac-5.2 {
execsql {
SELECT snippet(email) FROM email
WHERE email MATCH 'enron OR com questar'
}
} {{matt.smith@<b>enron</b>.<b>com</b> <b>...</b> six reports:
31 Keystone Receipts
} {{matt.smith@<b>enron</b>.<b>com</b><b>...</b>31 Keystone Receipts
15 <b>Questar</b> Pipeline
40 Rockies Production
22 West_2 <b>...</b>}}
40 Rockies<b>...</b>}}
finish_test

View File

@ -53,6 +53,10 @@ do_test fts3al-1.3 {
#
# The trailing and leading hi-bit chars help with code which tests for
# isspace() to coalesce multiple spaces.
#
# UPDATE: The above is no longer true; there is no such code in fts3.
# But leave the test in just the same.
#
set word "\x80xxxxx\x80xxxxx\x80xxxxx\x80xxxxx\x80xxxxx\x80xxxxx\x80"
set phrase1 "$word $word $word target $word $word $word"
@ -64,6 +68,6 @@ db eval "INSERT INTO t4 (content) VALUES ('$phrase2')"
do_test fts3al-1.4 {
execsql {SELECT rowid, length(snippet(t4)) FROM t4 WHERE t4 MATCH 'target'}
} {1 111 2 117}
} {1 241 2 247}
finish_test

View File

@ -76,6 +76,17 @@ do_test fts3near-1.15 {
execsql {SELECT docid FROM t1 WHERE content MATCH 'one NEAR two NEAR one'}
} {3}
do_test fts3near-1.16 {
execsql {
SELECT docid FROM t1 WHERE content MATCH '"one three" NEAR/0 "four five"'
}
} {1}
do_test fts3near-1.17 {
execsql {
SELECT docid FROM t1 WHERE content MATCH '"four five" NEAR/0 "one three"'
}
} {1}
# Output format of the offsets() function:
#
@ -154,6 +165,7 @@ do_test fts3near-3.6 {
SELECT offsets(t1) FROM t1 WHERE content MATCH 'three NEAR/0 "two four"'
}
} {{0 0 8 5 0 1 14 3 0 2 18 4}}
breakpoint
do_test fts3near-3.7 {
execsql {
SELECT offsets(t1) FROM t1 WHERE content MATCH '"two four" NEAR/0 three'}
@ -170,7 +182,7 @@ do_test fts3near-4.1 {
execsql {
SELECT snippet(t1) FROM t1 WHERE content MATCH 'specification NEAR supports'
}
} {{<b>...</b> devices, handheld devices, etc. This <b>specification</b> also <b>supports</b> content positioning, downloadable fonts, <b>...</b>}}
} {{<b>...</b>braille devices, handheld devices, etc. This <b>specification</b> also <b>supports</b> content positioning, downloadable fonts, table layout<b>...</b>}}
do_test fts3near-5.1 {
execsql {

68
test/fts3snippet.test Normal file
View File

@ -0,0 +1,68 @@
set testdir [file dirname $argv0]
source $testdir/tester.tcl
# If SQLITE_ENABLE_FTS3 is defined, omit this file.
ifcapable !fts3 { finish_test ; return }
do_test fts3snippet-1.1 {
execsql {
CREATE VIRTUAL TABLE ft USING fts3;
INSERT INTO ft VALUES('xxx xxx xxx xxx');
}
} {}
proc normalize {L} {
set ret [list]
foreach l $L {lappend ret $l}
return $ret
}
do_test fts3snippet-1.2 {
execsql { SELECT offsets(ft) FROM ft WHERE ft MATCH 'xxx' }
} {{0 0 0 3 0 0 4 3 0 0 8 3 0 0 12 3}}
do_test fts3snippet-1.3 {
execsql { SELECT offsets(ft) FROM ft WHERE ft MATCH '"xxx xxx"' }
} [list [normalize {
0 0 0 3
0 0 4 3
0 1 4 3
0 0 8 3
0 1 8 3
0 1 12 3
}]]
do_test fts3snippet-1.4 {
execsql { SELECT offsets(ft) FROM ft WHERE ft MATCH '"xxx xxx" xxx' }
} [list [normalize {
0 0 0 3
0 2 0 3
0 0 4 3
0 1 4 3
0 2 4 3
0 0 8 3
0 1 8 3
0 2 8 3
0 1 12 3
0 2 12 3
}]]
do_test fts3snippet-1.5 {
execsql { SELECT offsets(ft) FROM ft WHERE ft MATCH 'xxx "xxx xxx"' }
} [list [normalize {
0 0 0 3
0 1 0 3
0 0 4 3
0 1 4 3
0 2 4 3
0 0 8 3
0 1 8 3
0 2 8 3
0 0 12 3
0 2 12 3
}]]
finish_test