Fossil SCM
Document search now works with an index. Still no configuration screens for indexed search, however. full-scan search continues to work as before.
Commit
1bad221ecb5589703a500e99b63bb7902247d343
Parent
ec0e590191b10f2…
1 file changed
+92
-73
+92
-73
| --- src/search.c | ||
| +++ src/search.c | ||
| @@ -45,14 +45,15 @@ | ||
| 45 | 45 | char *zPattern; /* The search pattern */ |
| 46 | 46 | char *zMarkBegin; /* Start of a match */ |
| 47 | 47 | char *zMarkEnd; /* End of a match */ |
| 48 | 48 | char *zMarkGap; /* A gap between two matches */ |
| 49 | 49 | unsigned fSrchFlg; /* Flags */ |
| 50 | + int iScore; /* Score of the last match attempt */ | |
| 51 | + Blob snip; /* Snippet for the most recent match */ | |
| 50 | 52 | }; |
| 51 | 53 | |
| 52 | 54 | #define SRCHFLG_HTML 0x01 /* Escape snippet text for HTML */ |
| 53 | -#define SRCHFLG_SCORE 0x02 /* Prepend the score to each snippet */ | |
| 54 | 55 | #define SRCHFLG_STATIC 0x04 /* The static gSearch object */ |
| 55 | 56 | |
| 56 | 57 | #endif |
| 57 | 58 | |
| 58 | 59 | /* |
| @@ -92,10 +93,11 @@ | ||
| 92 | 93 | if( p ){ |
| 93 | 94 | fossil_free(p->zPattern); |
| 94 | 95 | fossil_free(p->zMarkBegin); |
| 95 | 96 | fossil_free(p->zMarkEnd); |
| 96 | 97 | fossil_free(p->zMarkGap); |
| 98 | + if( p->iScore ) blob_reset(&p->snip); | |
| 97 | 99 | memset(p, 0, sizeof(*p)); |
| 98 | 100 | if( p!=&gSearch ) fossil_free(p); |
| 99 | 101 | } |
| 100 | 102 | } |
| 101 | 103 | |
| @@ -123,10 +125,11 @@ | ||
| 123 | 125 | p->zPattern = z = mprintf("%s", zPattern); |
| 124 | 126 | p->zMarkBegin = mprintf("%s", zMarkBegin); |
| 125 | 127 | p->zMarkEnd = mprintf("%s", zMarkEnd); |
| 126 | 128 | p->zMarkGap = mprintf("%s", zMarkGap); |
| 127 | 129 | p->fSrchFlg = fSrchFlg; |
| 130 | + blob_init(&p->snip, 0, 0); | |
| 128 | 131 | while( *z && p->nTerm<SEARCH_MAX_TERM ){ |
| 129 | 132 | while( *z && !ISALNUM(*z) ){ z++; } |
| 130 | 133 | if( *z==0 ) break; |
| 131 | 134 | p->a[p->nTerm].z = z; |
| 132 | 135 | for(i=1; ISALNUM(z[i]); i++){} |
| @@ -156,24 +159,26 @@ | ||
| 156 | 159 | } |
| 157 | 160 | } |
| 158 | 161 | |
| 159 | 162 | /* |
| 160 | 163 | ** Compare a search pattern against one or more input strings which |
| 161 | -** collectively comprise a document. Return a match score. Optionally | |
| 162 | -** also return a "snippet". | |
| 164 | +** collectively comprise a document. Return a match score. Any | |
| 165 | +** postive value means there was a match. Zero means that one or | |
| 166 | +** more terms are missing. | |
| 167 | +** | |
| 168 | +** The score and a snippet are record for future use. | |
| 163 | 169 | ** |
| 164 | 170 | ** Scoring: |
| 165 | 171 | ** * All terms must match at least once or the score is zero |
| 166 | 172 | ** * One point for each matching term |
| 167 | 173 | ** * Extra points if consecutive words of the pattern are consecutive |
| 168 | 174 | ** in the document |
| 169 | 175 | */ |
| 170 | -static int search_score( | |
| 176 | +static int search_match( | |
| 171 | 177 | Search *p, /* Search pattern and flags */ |
| 172 | 178 | int nDoc, /* Number of strings in this document */ |
| 173 | - const char **azDoc, /* Text of each string */ | |
| 174 | - Blob *pSnip /* If not NULL: Write a snippet here */ | |
| 179 | + const char **azDoc /* Text of each string */ | |
| 175 | 180 | ){ |
| 176 | 181 | int score; /* Final score */ |
| 177 | 182 | int i; /* Offset into current document */ |
| 178 | 183 | int ii; /* Loop counter */ |
| 179 | 184 | int j; /* Loop over search terms */ |
| @@ -226,18 +231,17 @@ | ||
| 226 | 231 | /* Finished search all documents. |
| 227 | 232 | ** Every term must be seen or else the score is zero |
| 228 | 233 | */ |
| 229 | 234 | score = 1; |
| 230 | 235 | for(j=0; j<p->nTerm; j++) score *= anMatch[j]; |
| 231 | - if( score==0 || pSnip==0 ) return score; | |
| 236 | + blob_reset(&p->snip); | |
| 237 | + p->iScore = score; | |
| 238 | + if( score==0 ) return score; | |
| 232 | 239 | |
| 233 | 240 | |
| 234 | 241 | /* Prepare a snippet that describes the matching text. |
| 235 | 242 | */ |
| 236 | - blob_init(pSnip, 0, 0); | |
| 237 | - if( p->fSrchFlg & SRCHFLG_SCORE ) blob_appendf(pSnip, "%08x", score); | |
| 238 | - | |
| 239 | 243 | while(1){ |
| 240 | 244 | int iOfst; |
| 241 | 245 | int iTail; |
| 242 | 246 | int iBest; |
| 243 | 247 | for(ii=0; ii<p->nTerm && anMatch[ii]==0; ii++){} |
| @@ -272,11 +276,11 @@ | ||
| 272 | 276 | if( iOfst<0 ) iOfst = 0; |
| 273 | 277 | while( iOfst>0 && ISALNUM(zDoc[iOfst-1]) ) iOfst--; |
| 274 | 278 | while( zDoc[iOfst] && !ISALNUM(zDoc[iOfst]) ) iOfst++; |
| 275 | 279 | for(ii=0; ii<CTX && zDoc[iTail]; ii++, iTail++){} |
| 276 | 280 | while( ISALNUM(zDoc[iTail]) ) iTail++; |
| 277 | - if( iOfst>0 || wantGap ) blob_append(pSnip, p->zMarkGap, -1); | |
| 281 | + if( iOfst>0 || wantGap ) blob_append(&p->snip, p->zMarkGap, -1); | |
| 278 | 282 | wantGap = zDoc[iTail]!=0; |
| 279 | 283 | zDoc += iOfst; |
| 280 | 284 | iTail -= iOfst; |
| 281 | 285 | |
| 282 | 286 | /* Add a snippet segment using characters iOfst..iOfst+iTail from zDoc */ |
| @@ -285,53 +289,51 @@ | ||
| 285 | 289 | for(j=0; j<p->nTerm; j++){ |
| 286 | 290 | int n = p->a[j].n; |
| 287 | 291 | if( sqlite3_strnicmp(p->a[j].z, &zDoc[i], n)==0 |
| 288 | 292 | && (!ISALNUM(zDoc[i+n]) || p->a[j].z[n]=='*') |
| 289 | 293 | ){ |
| 290 | - snippet_text_append(p, pSnip, zDoc, i); | |
| 294 | + snippet_text_append(p, &p->snip, zDoc, i); | |
| 291 | 295 | zDoc += i; |
| 292 | 296 | iTail -= i; |
| 293 | - blob_append(pSnip, p->zMarkBegin, -1); | |
| 297 | + blob_append(&p->snip, p->zMarkBegin, -1); | |
| 294 | 298 | if( p->a[j].z[n]=='*' ){ |
| 295 | 299 | while( ISALNUM(zDoc[n]) ) n++; |
| 296 | 300 | } |
| 297 | - snippet_text_append(p, pSnip, zDoc, n); | |
| 301 | + snippet_text_append(p, &p->snip, zDoc, n); | |
| 298 | 302 | zDoc += n; |
| 299 | 303 | iTail -= n; |
| 300 | - blob_append(pSnip, p->zMarkEnd, -1); | |
| 304 | + blob_append(&p->snip, p->zMarkEnd, -1); | |
| 301 | 305 | i = -1; |
| 302 | 306 | break; |
| 303 | 307 | } /* end-if */ |
| 304 | 308 | } /* end for(j) */ |
| 305 | 309 | if( j<p->nTerm ){ |
| 306 | 310 | while( ISALNUM(zDoc[i]) && i<iTail ){ i++; } |
| 307 | 311 | } |
| 308 | 312 | } /* end for(i) */ |
| 309 | - snippet_text_append(p, pSnip, zDoc, iTail); | |
| 313 | + snippet_text_append(p, &p->snip, zDoc, iTail); | |
| 310 | 314 | } |
| 311 | - if( wantGap ) blob_append(pSnip, p->zMarkGap, -1); | |
| 315 | + if( wantGap ) blob_append(&p->snip, p->zMarkGap, -1); | |
| 312 | 316 | return score; |
| 313 | 317 | } |
| 314 | 318 | |
| 315 | 319 | /* |
| 316 | -** COMMAND: test-snippet | |
| 320 | +** COMMAND: test-match | |
| 317 | 321 | ** |
| 318 | -** Usage: fossil test-snippet SEARCHSTRING FILE1 FILE2 ... | |
| 322 | +** Usage: fossil test-match SEARCHSTRING FILE1 FILE2 ... | |
| 319 | 323 | */ |
| 320 | -void test_snippet_cmd(void){ | |
| 324 | +void test_match_cmd(void){ | |
| 321 | 325 | Search *p; |
| 322 | 326 | int i; |
| 323 | 327 | Blob x; |
| 324 | - Blob snip; | |
| 325 | 328 | int score; |
| 326 | 329 | char *zDoc; |
| 327 | 330 | int flg = 0; |
| 328 | 331 | char *zBegin = (char*)find_option("begin",0,1); |
| 329 | 332 | char *zEnd = (char*)find_option("end",0,1); |
| 330 | 333 | char *zGap = (char*)find_option("gap",0,1); |
| 331 | 334 | if( find_option("html",0,0)!=0 ) flg |= SRCHFLG_HTML; |
| 332 | - if( find_option("score",0,0)!=0 ) flg |= SRCHFLG_SCORE; | |
| 333 | 335 | if( find_option("static",0,0)!=0 ) flg |= SRCHFLG_STATIC; |
| 334 | 336 | verify_all_options(); |
| 335 | 337 | if( g.argc<4 ) usage("SEARCHSTRING FILE1..."); |
| 336 | 338 | if( zBegin==0 ) zBegin = "[["; |
| 337 | 339 | if( zEnd==0 ) zEnd = "]]"; |
| @@ -338,18 +340,18 @@ | ||
| 338 | 340 | if( zGap==0 ) zGap = " ... "; |
| 339 | 341 | p = search_init(g.argv[2], zBegin, zEnd, zGap, flg); |
| 340 | 342 | for(i=3; i<g.argc; i++){ |
| 341 | 343 | blob_read_from_file(&x, g.argv[i]); |
| 342 | 344 | zDoc = blob_str(&x); |
| 343 | - score = search_score(p, 1, (const char**)&zDoc, &snip); | |
| 344 | - fossil_print("%s: %d\n", g.argv[i], score); | |
| 345 | + score = search_match(p, 1, (const char**)&zDoc); | |
| 346 | + fossil_print("%s: %d\n", g.argv[i], p->iScore); | |
| 345 | 347 | blob_reset(&x); |
| 346 | 348 | if( score ){ |
| 347 | - fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&snip), '='); | |
| 348 | - blob_reset(&snip); | |
| 349 | + fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&p->snip), '='); | |
| 349 | 350 | } |
| 350 | 351 | } |
| 352 | + search_end(p); | |
| 351 | 353 | } |
| 352 | 354 | |
| 353 | 355 | /* |
| 354 | 356 | ** An SQL function to initialize the global search pattern: |
| 355 | 357 | ** |
| @@ -385,35 +387,45 @@ | ||
| 385 | 387 | search_end(&gSearch); |
| 386 | 388 | } |
| 387 | 389 | } |
| 388 | 390 | |
| 389 | 391 | /* |
| 390 | -** This is an SQLite function that scores its input using | |
| 391 | -** the pattern from the previous call to search_init(). | |
| 392 | +** Try to match the input text against the search parameters set up | |
| 393 | +** by the previous search_init() call. Remember the results globally. | |
| 394 | +** Return non-zero on a match and zero on a miss. | |
| 395 | +*/ | |
| 396 | +static void search_match_sqlfunc( | |
| 397 | + sqlite3_context *context, | |
| 398 | + int argc, | |
| 399 | + sqlite3_value **argv | |
| 400 | +){ | |
| 401 | + const char *zSText = (const char*)sqlite3_value_text(argv[0]); | |
| 402 | + int rc; | |
| 403 | + if( zSText==0 ) return; | |
| 404 | + rc = search_match(&gSearch, 1, &zSText); | |
| 405 | + sqlite3_result_int(context, rc); | |
| 406 | +} | |
| 407 | + | |
| 408 | +/* | |
| 409 | +** These SQL functions return the results of the last | |
| 410 | +** call to the search_match() SQL function. | |
| 392 | 411 | */ |
| 393 | 412 | static void search_score_sqlfunc( |
| 394 | 413 | sqlite3_context *context, |
| 395 | 414 | int argc, |
| 396 | 415 | sqlite3_value **argv |
| 397 | 416 | ){ |
| 398 | - int isSnippet = sqlite3_user_data(context)!=0; | |
| 399 | - const char **azDoc; | |
| 400 | - int score; | |
| 401 | - int i; | |
| 402 | - Blob snip; | |
| 403 | - | |
| 404 | - if( gSearch.nTerm==0 ) return; | |
| 405 | - azDoc = fossil_malloc( sizeof(const char*)*(argc+1) ); | |
| 406 | - for(i=0; i<argc; i++) azDoc[i] = (const char*)sqlite3_value_text(argv[i]); | |
| 407 | - score = search_score(&gSearch, argc, azDoc, isSnippet ? &snip : 0); | |
| 408 | - fossil_free((void *)azDoc); | |
| 409 | - if( isSnippet ){ | |
| 410 | - if( score ){ | |
| 411 | - sqlite3_result_text(context, blob_materialize(&snip), -1, fossil_free); | |
| 412 | - } | |
| 413 | - }else{ | |
| 414 | - sqlite3_result_int(context, score); | |
| 417 | + sqlite3_result_int(context, gSearch.iScore); | |
| 418 | +} | |
| 419 | +static void search_snippet_sqlfunc( | |
| 420 | + sqlite3_context *context, | |
| 421 | + int argc, | |
| 422 | + sqlite3_value **argv | |
| 423 | +){ | |
| 424 | + if( blob_size(&gSearch.snip)>0 ){ | |
| 425 | + sqlite3_result_text(context, blob_str(&gSearch.snip), -1, fossil_free); | |
| 426 | + blob_init(&gSearch.snip, 0, 0); | |
| 415 | 427 | } |
| 416 | 428 | } |
| 417 | 429 | |
| 418 | 430 | /* |
| 419 | 431 | ** This is an SQLite function that computes the searchable text. |
| @@ -451,14 +463,16 @@ | ||
| 451 | 463 | ** do not delete the Search object. |
| 452 | 464 | */ |
| 453 | 465 | void search_sql_setup(sqlite3 *db){ |
| 454 | 466 | static int once = 0; |
| 455 | 467 | if( once++ ) return; |
| 456 | - sqlite3_create_function(db, "score", -1, SQLITE_UTF8, 0, | |
| 457 | - search_score_sqlfunc, 0, 0); | |
| 458 | - sqlite3_create_function(db, "fsnippet", -1, SQLITE_UTF8, &gSearch, | |
| 468 | + sqlite3_create_function(db, "search_match", 1, SQLITE_UTF8, 0, | |
| 469 | + search_match_sqlfunc, 0, 0); | |
| 470 | + sqlite3_create_function(db, "search_score", 0, SQLITE_UTF8, 0, | |
| 459 | 471 | search_score_sqlfunc, 0, 0); |
| 472 | + sqlite3_create_function(db, "search_snippet", 0, SQLITE_UTF8, 0, | |
| 473 | + search_snippet_sqlfunc, 0, 0); | |
| 460 | 474 | sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0, |
| 461 | 475 | search_init_sqlfunc, 0, 0); |
| 462 | 476 | sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0, |
| 463 | 477 | search_stext_sqlfunc, 0, 0); |
| 464 | 478 | sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0, |
| @@ -587,28 +601,30 @@ | ||
| 587 | 601 | static void search_fullscan( |
| 588 | 602 | const char *zPattern, /* The query pattern */ |
| 589 | 603 | unsigned int srchFlags /* What to search over */ |
| 590 | 604 | ){ |
| 591 | 605 | search_init(zPattern, "<b>", "</b>", " ... ", |
| 592 | - SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE); | |
| 606 | + SRCHFLG_STATIC|SRCHFLG_HTML); | |
| 593 | 607 | if( (srchFlags & SRCH_DOC)!=0 ){ |
| 594 | 608 | char *zDocGlob = db_get("doc-glob",""); |
| 595 | 609 | char *zDocBr = db_get("doc-branch","trunk"); |
| 596 | 610 | if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){ |
| 597 | 611 | db_multi_exec( |
| 598 | 612 | "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 599 | 613 | ); |
| 600 | 614 | db_multi_exec( |
| 601 | - "INSERT INTO x(label,url,date,snip)" | |
| 615 | + "INSERT INTO x(label,url,score,date,snip)" | |
| 602 | 616 | " SELECT printf('Document: %%s',foci.filename)," |
| 603 | 617 | " printf('%R/doc/%T/%%s',foci.filename)," |
| 618 | + " search_score()," | |
| 604 | 619 | " (SELECT datetime(event.mtime) FROM event" |
| 605 | 620 | " WHERE objid=symbolic_name_to_rid('trunk'))," |
| 606 | - " fsnippet(stext('d',blob.rid,foci.filename))" | |
| 621 | + " search_snippet()" | |
| 607 | 622 | " FROM foci CROSS JOIN blob" |
| 608 | 623 | " WHERE checkinID=symbolic_name_to_rid('trunk')" |
| 609 | 624 | " AND blob.uuid=foci.uuid" |
| 625 | + " AND search_match(stext('d',blob.rid,foci.filename))" | |
| 610 | 626 | " AND %z", |
| 611 | 627 | zDocBr, glob_expr("foci.filename", zDocGlob) |
| 612 | 628 | ); |
| 613 | 629 | } |
| 614 | 630 | } |
| @@ -619,16 +635,18 @@ | ||
| 619 | 635 | " FROM tag, tagxref" |
| 620 | 636 | " WHERE tag.tagname GLOB 'wiki-*'" |
| 621 | 637 | " AND tagxref.tagid=tag.tagid" |
| 622 | 638 | " GROUP BY 1" |
| 623 | 639 | ")" |
| 624 | - "INSERT INTO x(label,url,date,snip)" | |
| 640 | + "INSERT INTO x(label,url,score,date,snip)" | |
| 625 | 641 | " SELECT printf('Wiki: %%s',name)," |
| 626 | 642 | " printf('%R/wiki?name=%%s',urlencode(name))," |
| 643 | + " search_score()," | |
| 627 | 644 | " datetime(mtime)," |
| 628 | - " fsnippet(stext('w',rid,name))" | |
| 629 | - " FROM wiki;" | |
| 645 | + " search_snippet()" | |
| 646 | + " FROM wiki" | |
| 647 | + " WHERE search_match(stext('w',rid,name));" | |
| 630 | 648 | ); |
| 631 | 649 | } |
| 632 | 650 | if( (srchFlags & SRCH_CKIN)!=0 ){ |
| 633 | 651 | db_multi_exec( |
| 634 | 652 | "WITH ckin(uuid,rid,mtime) AS (" |
| @@ -635,32 +653,33 @@ | ||
| 635 | 653 | " SELECT blob.uuid, event.objid, event.mtime" |
| 636 | 654 | " FROM event, blob" |
| 637 | 655 | " WHERE event.type='ci'" |
| 638 | 656 | " AND blob.rid=event.objid" |
| 639 | 657 | ")" |
| 640 | - "INSERT INTO x(label,url,date,snip)" | |
| 658 | + "INSERT INTO x(label,url,score,date,snip)" | |
| 641 | 659 | " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime))," |
| 642 | 660 | " printf('%R/timeline?c=%%s&n=8&y=ci',uuid)," |
| 661 | + " search_score()," | |
| 643 | 662 | " datetime(mtime)," |
| 644 | - " fsnippet(stext('c',rid,NULL))" | |
| 645 | - " FROM ckin;" | |
| 663 | + " search_snippet()" | |
| 664 | + " FROM ckin" | |
| 665 | + " WHERE search_match(stext('c',rid,NULL));" | |
| 646 | 666 | ); |
| 647 | 667 | } |
| 648 | 668 | if( (srchFlags & SRCH_TKT)!=0 ){ |
| 649 | 669 | db_multi_exec( |
| 650 | - "INSERT INTO x(label,url,date,snip)" | |
| 670 | + "INSERT INTO x(label,url,score, date,snip)" | |
| 651 | 671 | " SELECT printf('Ticket [%%.17s] on %%s'," |
| 652 | 672 | "tkt_uuid,datetime(tkt_mtime))," |
| 653 | 673 | " printf('%R/tktview/%%.20s',tkt_uuid)," |
| 674 | + " search_score()," | |
| 654 | 675 | " datetime(tkt_mtime)," |
| 655 | - " fsnippet(stext('t',tkt_id,NULL))" | |
| 656 | - " FROM ticket;" | |
| 676 | + " search_snippet()" | |
| 677 | + " FROM ticket" | |
| 678 | + " WHERE search_match(stext('t',tkt_id,NULL));" | |
| 657 | 679 | ); |
| 658 | 680 | } |
| 659 | - db_multi_exec( | |
| 660 | - "UPDATE x SET score=substr(snip,1,8), snip=substr(snip,9)" | |
| 661 | - ); | |
| 662 | 681 | } |
| 663 | 682 | |
| 664 | 683 | /* |
| 665 | 684 | ** When this routine is called, there already exists a table |
| 666 | 685 | ** |
| @@ -679,14 +698,14 @@ | ||
| 679 | 698 | "INSERT INTO x(label,url,score,date,snip) " |
| 680 | 699 | " SELECT ftsdocs.label," |
| 681 | 700 | " ftsdocs.url," |
| 682 | 701 | " 1," /*FIX ME*/ |
| 683 | 702 | " datetime(ftsdocs.mtime)," |
| 684 | - " fsnippet(ftsidx,'<b>','</b>',' ... ')" | |
| 703 | + " snippet(ftsidx)" | |
| 685 | 704 | " FROM ftsidx, ftsdocs" |
| 686 | 705 | " WHERE ftsidx MATCH %Q" |
| 687 | - " AND ftsdocs.id=ftsidx.docid", | |
| 706 | + " AND ftsdocs.rowid=ftsidx.docid", | |
| 688 | 707 | zPattern |
| 689 | 708 | ); |
| 690 | 709 | } |
| 691 | 710 | |
| 692 | 711 | |
| @@ -981,11 +1000,11 @@ | ||
| 981 | 1000 | /* The schema for the full-text index |
| 982 | 1001 | */ |
| 983 | 1002 | static const char zFtsSchema[] = |
| 984 | 1003 | @ -- One entry for each possible search result |
| 985 | 1004 | @ CREATE TABLE IF NOT EXISTS "%w".ftsdocs( |
| 986 | -@ id INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid | |
| 1005 | +@ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid | |
| 987 | 1006 | @ type CHAR(1), -- Type of document |
| 988 | 1007 | @ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document |
| 989 | 1008 | @ name TEXT, -- Additional document description |
| 990 | 1009 | @ idxed BOOLEAN, -- True if currently in the index |
| 991 | 1010 | @ label TEXT, -- Label to print on search results |
| @@ -993,11 +1012,11 @@ | ||
| 993 | 1012 | @ mtime DATE, -- Date when document created |
| 994 | 1013 | @ UNIQUE(type,rid) |
| 995 | 1014 | @ ); |
| 996 | 1015 | @ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0; |
| 997 | 1016 | @ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS |
| 998 | -@ SELECT id, type, rid, name, idxed, label, url, mtime,fr 5 | |
| 1017 | +@ SELECT rowid, type, rid, name, idxed, label, url, mtime, | |
| 999 | 1018 | @ stext(type,rid,name) AS 'stext' |
| 1000 | 1019 | @ FROM ftsdocs; |
| 1001 | 1020 | @ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx |
| 1002 | 1021 | @ USING fts4(content="ftscontent", stext); |
| 1003 | 1022 | ; |
| @@ -1066,11 +1085,11 @@ | ||
| 1066 | 1085 | */ |
| 1067 | 1086 | void search_doc_touch(char cType, int rid, const char *zName){ |
| 1068 | 1087 | if( search_index_exists() ){ |
| 1069 | 1088 | db_multi_exec( |
| 1070 | 1089 | "DELETE FROM ftsidx WHERE docid IN" |
| 1071 | - " (SELECT id FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)", | |
| 1090 | + " (SELECT rowid FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)", | |
| 1072 | 1091 | cType, rid |
| 1073 | 1092 | ); |
| 1074 | 1093 | db_multi_exec( |
| 1075 | 1094 | "REPLACE INTO ftsdocs(type,rid,name,idxed)" |
| 1076 | 1095 | " VALUES(%Q,%d,%Q,0)", |
| @@ -1107,11 +1126,11 @@ | ||
| 1107 | 1126 | " AND %z", |
| 1108 | 1127 | ckid, glob_expr("foci.filename", db_get("doc-glob","")) |
| 1109 | 1128 | ); |
| 1110 | 1129 | db_multi_exec( |
| 1111 | 1130 | "DELETE FROM ftsidx WHERE docid IN" |
| 1112 | - " (SELECT id FROM ftsdocs WHERE type='d'" | |
| 1131 | + " (SELECT rowid FROM ftsdocs WHERE type='d'" | |
| 1113 | 1132 | " AND rid NOT IN (SELECT rid FROM current_docs))" |
| 1114 | 1133 | ); |
| 1115 | 1134 | db_multi_exec( |
| 1116 | 1135 | "DELETE FROM ftsdocs WHERE type='d'" |
| 1117 | 1136 | " AND rid NOT IN (SELECT rid FROM current_docs)" |
| @@ -1125,11 +1144,11 @@ | ||
| 1125 | 1144 | " FROM current_docs", |
| 1126 | 1145 | zBrUuid, rTime |
| 1127 | 1146 | ); |
| 1128 | 1147 | db_multi_exec( |
| 1129 | 1148 | "INSERT INTO ftsidx(docid,stext)" |
| 1130 | - " SELECT id, stext FROM ftscontent WHERE type='d' AND NOT idxed" | |
| 1149 | + " SELECT rowid, stext FROM ftscontent WHERE type='d' AND NOT idxed" | |
| 1131 | 1150 | ); |
| 1132 | 1151 | db_multi_exec( |
| 1133 | 1152 | "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed" |
| 1134 | 1153 | ); |
| 1135 | 1154 | } |
| @@ -1184,11 +1203,11 @@ | ||
| 1184 | 1203 | } |
| 1185 | 1204 | case 2: { assert( fossil_strncmp(zSubCmd, "drop", n)==0 ); |
| 1186 | 1205 | search_drop_index(); |
| 1187 | 1206 | break; |
| 1188 | 1207 | } |
| 1189 | - case 3: { assert( fossil_strncmp(zSubCmd, "exist", n)==0 ); | |
| 1208 | + case 3: { assert( fossil_strncmp(zSubCmd, "exists", n)==0 ); | |
| 1190 | 1209 | fossil_print("search_index_exists() = %d\n", search_index_exists()); |
| 1191 | 1210 | break; |
| 1192 | 1211 | } |
| 1193 | 1212 | case 4: { assert( fossil_strncmp(zSubCmd, "fill", n)==0 ); |
| 1194 | 1213 | search_fill_index(); |
| @@ -1201,11 +1220,11 @@ | ||
| 1201 | 1220 | break; |
| 1202 | 1221 | } |
| 1203 | 1222 | case 5: { assert( fossil_strncmp(zSubCmd, "pending", n)==0 ); |
| 1204 | 1223 | Stmt q; |
| 1205 | 1224 | if( !search_index_exists() ) break; |
| 1206 | - db_prepare(&q, "SELECT id, type, rid, quote(label), url, date(mtime)" | |
| 1225 | + db_prepare(&q, "SELECT rowid, type, rid, quote(label), url, date(mtime)" | |
| 1207 | 1226 | " FROM ftsdocs" |
| 1208 | 1227 | " WHERE NOT idxed"); |
| 1209 | 1228 | while( db_step(&q)==SQLITE_ROW ){ |
| 1210 | 1229 | const char *zUrl = db_column_text(&q,4); |
| 1211 | 1230 | if( zUrl && zUrl[0] ){ |
| @@ -1229,11 +1248,11 @@ | ||
| 1229 | 1248 | break; |
| 1230 | 1249 | } |
| 1231 | 1250 | case 6: { assert( fossil_strncmp(zSubCmd, "all", n)==0 ); |
| 1232 | 1251 | Stmt q; |
| 1233 | 1252 | if( !search_index_exists() ) break; |
| 1234 | - db_prepare(&q, "SELECT id, type, rid, quote(name), idxed FROM ftsdocs"); | |
| 1253 | + db_prepare(&q, "SELECT rowid,type,rid,quote(name),idxed FROM ftsdocs"); | |
| 1235 | 1254 | while( db_step(&q)==SQLITE_ROW ){ |
| 1236 | 1255 | fossil_print("%6d: %s %6d %s%s\n", |
| 1237 | 1256 | db_column_int(&q, 0), |
| 1238 | 1257 | db_column_text(&q, 1), |
| 1239 | 1258 | db_column_int(&q, 2), |
| 1240 | 1259 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -45,14 +45,15 @@ | |
| 45 | char *zPattern; /* The search pattern */ |
| 46 | char *zMarkBegin; /* Start of a match */ |
| 47 | char *zMarkEnd; /* End of a match */ |
| 48 | char *zMarkGap; /* A gap between two matches */ |
| 49 | unsigned fSrchFlg; /* Flags */ |
| 50 | }; |
| 51 | |
| 52 | #define SRCHFLG_HTML 0x01 /* Escape snippet text for HTML */ |
| 53 | #define SRCHFLG_SCORE 0x02 /* Prepend the score to each snippet */ |
| 54 | #define SRCHFLG_STATIC 0x04 /* The static gSearch object */ |
| 55 | |
| 56 | #endif |
| 57 | |
| 58 | /* |
| @@ -92,10 +93,11 @@ | |
| 92 | if( p ){ |
| 93 | fossil_free(p->zPattern); |
| 94 | fossil_free(p->zMarkBegin); |
| 95 | fossil_free(p->zMarkEnd); |
| 96 | fossil_free(p->zMarkGap); |
| 97 | memset(p, 0, sizeof(*p)); |
| 98 | if( p!=&gSearch ) fossil_free(p); |
| 99 | } |
| 100 | } |
| 101 | |
| @@ -123,10 +125,11 @@ | |
| 123 | p->zPattern = z = mprintf("%s", zPattern); |
| 124 | p->zMarkBegin = mprintf("%s", zMarkBegin); |
| 125 | p->zMarkEnd = mprintf("%s", zMarkEnd); |
| 126 | p->zMarkGap = mprintf("%s", zMarkGap); |
| 127 | p->fSrchFlg = fSrchFlg; |
| 128 | while( *z && p->nTerm<SEARCH_MAX_TERM ){ |
| 129 | while( *z && !ISALNUM(*z) ){ z++; } |
| 130 | if( *z==0 ) break; |
| 131 | p->a[p->nTerm].z = z; |
| 132 | for(i=1; ISALNUM(z[i]); i++){} |
| @@ -156,24 +159,26 @@ | |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | /* |
| 160 | ** Compare a search pattern against one or more input strings which |
| 161 | ** collectively comprise a document. Return a match score. Optionally |
| 162 | ** also return a "snippet". |
| 163 | ** |
| 164 | ** Scoring: |
| 165 | ** * All terms must match at least once or the score is zero |
| 166 | ** * One point for each matching term |
| 167 | ** * Extra points if consecutive words of the pattern are consecutive |
| 168 | ** in the document |
| 169 | */ |
| 170 | static int search_score( |
| 171 | Search *p, /* Search pattern and flags */ |
| 172 | int nDoc, /* Number of strings in this document */ |
| 173 | const char **azDoc, /* Text of each string */ |
| 174 | Blob *pSnip /* If not NULL: Write a snippet here */ |
| 175 | ){ |
| 176 | int score; /* Final score */ |
| 177 | int i; /* Offset into current document */ |
| 178 | int ii; /* Loop counter */ |
| 179 | int j; /* Loop over search terms */ |
| @@ -226,18 +231,17 @@ | |
| 226 | /* Finished search all documents. |
| 227 | ** Every term must be seen or else the score is zero |
| 228 | */ |
| 229 | score = 1; |
| 230 | for(j=0; j<p->nTerm; j++) score *= anMatch[j]; |
| 231 | if( score==0 || pSnip==0 ) return score; |
| 232 | |
| 233 | |
| 234 | /* Prepare a snippet that describes the matching text. |
| 235 | */ |
| 236 | blob_init(pSnip, 0, 0); |
| 237 | if( p->fSrchFlg & SRCHFLG_SCORE ) blob_appendf(pSnip, "%08x", score); |
| 238 | |
| 239 | while(1){ |
| 240 | int iOfst; |
| 241 | int iTail; |
| 242 | int iBest; |
| 243 | for(ii=0; ii<p->nTerm && anMatch[ii]==0; ii++){} |
| @@ -272,11 +276,11 @@ | |
| 272 | if( iOfst<0 ) iOfst = 0; |
| 273 | while( iOfst>0 && ISALNUM(zDoc[iOfst-1]) ) iOfst--; |
| 274 | while( zDoc[iOfst] && !ISALNUM(zDoc[iOfst]) ) iOfst++; |
| 275 | for(ii=0; ii<CTX && zDoc[iTail]; ii++, iTail++){} |
| 276 | while( ISALNUM(zDoc[iTail]) ) iTail++; |
| 277 | if( iOfst>0 || wantGap ) blob_append(pSnip, p->zMarkGap, -1); |
| 278 | wantGap = zDoc[iTail]!=0; |
| 279 | zDoc += iOfst; |
| 280 | iTail -= iOfst; |
| 281 | |
| 282 | /* Add a snippet segment using characters iOfst..iOfst+iTail from zDoc */ |
| @@ -285,53 +289,51 @@ | |
| 285 | for(j=0; j<p->nTerm; j++){ |
| 286 | int n = p->a[j].n; |
| 287 | if( sqlite3_strnicmp(p->a[j].z, &zDoc[i], n)==0 |
| 288 | && (!ISALNUM(zDoc[i+n]) || p->a[j].z[n]=='*') |
| 289 | ){ |
| 290 | snippet_text_append(p, pSnip, zDoc, i); |
| 291 | zDoc += i; |
| 292 | iTail -= i; |
| 293 | blob_append(pSnip, p->zMarkBegin, -1); |
| 294 | if( p->a[j].z[n]=='*' ){ |
| 295 | while( ISALNUM(zDoc[n]) ) n++; |
| 296 | } |
| 297 | snippet_text_append(p, pSnip, zDoc, n); |
| 298 | zDoc += n; |
| 299 | iTail -= n; |
| 300 | blob_append(pSnip, p->zMarkEnd, -1); |
| 301 | i = -1; |
| 302 | break; |
| 303 | } /* end-if */ |
| 304 | } /* end for(j) */ |
| 305 | if( j<p->nTerm ){ |
| 306 | while( ISALNUM(zDoc[i]) && i<iTail ){ i++; } |
| 307 | } |
| 308 | } /* end for(i) */ |
| 309 | snippet_text_append(p, pSnip, zDoc, iTail); |
| 310 | } |
| 311 | if( wantGap ) blob_append(pSnip, p->zMarkGap, -1); |
| 312 | return score; |
| 313 | } |
| 314 | |
| 315 | /* |
| 316 | ** COMMAND: test-snippet |
| 317 | ** |
| 318 | ** Usage: fossil test-snippet SEARCHSTRING FILE1 FILE2 ... |
| 319 | */ |
| 320 | void test_snippet_cmd(void){ |
| 321 | Search *p; |
| 322 | int i; |
| 323 | Blob x; |
| 324 | Blob snip; |
| 325 | int score; |
| 326 | char *zDoc; |
| 327 | int flg = 0; |
| 328 | char *zBegin = (char*)find_option("begin",0,1); |
| 329 | char *zEnd = (char*)find_option("end",0,1); |
| 330 | char *zGap = (char*)find_option("gap",0,1); |
| 331 | if( find_option("html",0,0)!=0 ) flg |= SRCHFLG_HTML; |
| 332 | if( find_option("score",0,0)!=0 ) flg |= SRCHFLG_SCORE; |
| 333 | if( find_option("static",0,0)!=0 ) flg |= SRCHFLG_STATIC; |
| 334 | verify_all_options(); |
| 335 | if( g.argc<4 ) usage("SEARCHSTRING FILE1..."); |
| 336 | if( zBegin==0 ) zBegin = "[["; |
| 337 | if( zEnd==0 ) zEnd = "]]"; |
| @@ -338,18 +340,18 @@ | |
| 338 | if( zGap==0 ) zGap = " ... "; |
| 339 | p = search_init(g.argv[2], zBegin, zEnd, zGap, flg); |
| 340 | for(i=3; i<g.argc; i++){ |
| 341 | blob_read_from_file(&x, g.argv[i]); |
| 342 | zDoc = blob_str(&x); |
| 343 | score = search_score(p, 1, (const char**)&zDoc, &snip); |
| 344 | fossil_print("%s: %d\n", g.argv[i], score); |
| 345 | blob_reset(&x); |
| 346 | if( score ){ |
| 347 | fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&snip), '='); |
| 348 | blob_reset(&snip); |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /* |
| 354 | ** An SQL function to initialize the global search pattern: |
| 355 | ** |
| @@ -385,35 +387,45 @@ | |
| 385 | search_end(&gSearch); |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | /* |
| 390 | ** This is an SQLite function that scores its input using |
| 391 | ** the pattern from the previous call to search_init(). |
| 392 | */ |
| 393 | static void search_score_sqlfunc( |
| 394 | sqlite3_context *context, |
| 395 | int argc, |
| 396 | sqlite3_value **argv |
| 397 | ){ |
| 398 | int isSnippet = sqlite3_user_data(context)!=0; |
| 399 | const char **azDoc; |
| 400 | int score; |
| 401 | int i; |
| 402 | Blob snip; |
| 403 | |
| 404 | if( gSearch.nTerm==0 ) return; |
| 405 | azDoc = fossil_malloc( sizeof(const char*)*(argc+1) ); |
| 406 | for(i=0; i<argc; i++) azDoc[i] = (const char*)sqlite3_value_text(argv[i]); |
| 407 | score = search_score(&gSearch, argc, azDoc, isSnippet ? &snip : 0); |
| 408 | fossil_free((void *)azDoc); |
| 409 | if( isSnippet ){ |
| 410 | if( score ){ |
| 411 | sqlite3_result_text(context, blob_materialize(&snip), -1, fossil_free); |
| 412 | } |
| 413 | }else{ |
| 414 | sqlite3_result_int(context, score); |
| 415 | } |
| 416 | } |
| 417 | |
| 418 | /* |
| 419 | ** This is an SQLite function that computes the searchable text. |
| @@ -451,14 +463,16 @@ | |
| 451 | ** do not delete the Search object. |
| 452 | */ |
| 453 | void search_sql_setup(sqlite3 *db){ |
| 454 | static int once = 0; |
| 455 | if( once++ ) return; |
| 456 | sqlite3_create_function(db, "score", -1, SQLITE_UTF8, 0, |
| 457 | search_score_sqlfunc, 0, 0); |
| 458 | sqlite3_create_function(db, "fsnippet", -1, SQLITE_UTF8, &gSearch, |
| 459 | search_score_sqlfunc, 0, 0); |
| 460 | sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0, |
| 461 | search_init_sqlfunc, 0, 0); |
| 462 | sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0, |
| 463 | search_stext_sqlfunc, 0, 0); |
| 464 | sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0, |
| @@ -587,28 +601,30 @@ | |
| 587 | static void search_fullscan( |
| 588 | const char *zPattern, /* The query pattern */ |
| 589 | unsigned int srchFlags /* What to search over */ |
| 590 | ){ |
| 591 | search_init(zPattern, "<b>", "</b>", " ... ", |
| 592 | SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE); |
| 593 | if( (srchFlags & SRCH_DOC)!=0 ){ |
| 594 | char *zDocGlob = db_get("doc-glob",""); |
| 595 | char *zDocBr = db_get("doc-branch","trunk"); |
| 596 | if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){ |
| 597 | db_multi_exec( |
| 598 | "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 599 | ); |
| 600 | db_multi_exec( |
| 601 | "INSERT INTO x(label,url,date,snip)" |
| 602 | " SELECT printf('Document: %%s',foci.filename)," |
| 603 | " printf('%R/doc/%T/%%s',foci.filename)," |
| 604 | " (SELECT datetime(event.mtime) FROM event" |
| 605 | " WHERE objid=symbolic_name_to_rid('trunk'))," |
| 606 | " fsnippet(stext('d',blob.rid,foci.filename))" |
| 607 | " FROM foci CROSS JOIN blob" |
| 608 | " WHERE checkinID=symbolic_name_to_rid('trunk')" |
| 609 | " AND blob.uuid=foci.uuid" |
| 610 | " AND %z", |
| 611 | zDocBr, glob_expr("foci.filename", zDocGlob) |
| 612 | ); |
| 613 | } |
| 614 | } |
| @@ -619,16 +635,18 @@ | |
| 619 | " FROM tag, tagxref" |
| 620 | " WHERE tag.tagname GLOB 'wiki-*'" |
| 621 | " AND tagxref.tagid=tag.tagid" |
| 622 | " GROUP BY 1" |
| 623 | ")" |
| 624 | "INSERT INTO x(label,url,date,snip)" |
| 625 | " SELECT printf('Wiki: %%s',name)," |
| 626 | " printf('%R/wiki?name=%%s',urlencode(name))," |
| 627 | " datetime(mtime)," |
| 628 | " fsnippet(stext('w',rid,name))" |
| 629 | " FROM wiki;" |
| 630 | ); |
| 631 | } |
| 632 | if( (srchFlags & SRCH_CKIN)!=0 ){ |
| 633 | db_multi_exec( |
| 634 | "WITH ckin(uuid,rid,mtime) AS (" |
| @@ -635,32 +653,33 @@ | |
| 635 | " SELECT blob.uuid, event.objid, event.mtime" |
| 636 | " FROM event, blob" |
| 637 | " WHERE event.type='ci'" |
| 638 | " AND blob.rid=event.objid" |
| 639 | ")" |
| 640 | "INSERT INTO x(label,url,date,snip)" |
| 641 | " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime))," |
| 642 | " printf('%R/timeline?c=%%s&n=8&y=ci',uuid)," |
| 643 | " datetime(mtime)," |
| 644 | " fsnippet(stext('c',rid,NULL))" |
| 645 | " FROM ckin;" |
| 646 | ); |
| 647 | } |
| 648 | if( (srchFlags & SRCH_TKT)!=0 ){ |
| 649 | db_multi_exec( |
| 650 | "INSERT INTO x(label,url,date,snip)" |
| 651 | " SELECT printf('Ticket [%%.17s] on %%s'," |
| 652 | "tkt_uuid,datetime(tkt_mtime))," |
| 653 | " printf('%R/tktview/%%.20s',tkt_uuid)," |
| 654 | " datetime(tkt_mtime)," |
| 655 | " fsnippet(stext('t',tkt_id,NULL))" |
| 656 | " FROM ticket;" |
| 657 | ); |
| 658 | } |
| 659 | db_multi_exec( |
| 660 | "UPDATE x SET score=substr(snip,1,8), snip=substr(snip,9)" |
| 661 | ); |
| 662 | } |
| 663 | |
| 664 | /* |
| 665 | ** When this routine is called, there already exists a table |
| 666 | ** |
| @@ -679,14 +698,14 @@ | |
| 679 | "INSERT INTO x(label,url,score,date,snip) " |
| 680 | " SELECT ftsdocs.label," |
| 681 | " ftsdocs.url," |
| 682 | " 1," /*FIX ME*/ |
| 683 | " datetime(ftsdocs.mtime)," |
| 684 | " fsnippet(ftsidx,'<b>','</b>',' ... ')" |
| 685 | " FROM ftsidx, ftsdocs" |
| 686 | " WHERE ftsidx MATCH %Q" |
| 687 | " AND ftsdocs.id=ftsidx.docid", |
| 688 | zPattern |
| 689 | ); |
| 690 | } |
| 691 | |
| 692 | |
| @@ -981,11 +1000,11 @@ | |
| 981 | /* The schema for the full-text index |
| 982 | */ |
| 983 | static const char zFtsSchema[] = |
| 984 | @ -- One entry for each possible search result |
| 985 | @ CREATE TABLE IF NOT EXISTS "%w".ftsdocs( |
| 986 | @ id INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid |
| 987 | @ type CHAR(1), -- Type of document |
| 988 | @ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document |
| 989 | @ name TEXT, -- Additional document description |
| 990 | @ idxed BOOLEAN, -- True if currently in the index |
| 991 | @ label TEXT, -- Label to print on search results |
| @@ -993,11 +1012,11 @@ | |
| 993 | @ mtime DATE, -- Date when document created |
| 994 | @ UNIQUE(type,rid) |
| 995 | @ ); |
| 996 | @ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0; |
| 997 | @ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS |
| 998 | @ SELECT id, type, rid, name, idxed, label, url, mtime,fr 5 |
| 999 | @ stext(type,rid,name) AS 'stext' |
| 1000 | @ FROM ftsdocs; |
| 1001 | @ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx |
| 1002 | @ USING fts4(content="ftscontent", stext); |
| 1003 | ; |
| @@ -1066,11 +1085,11 @@ | |
| 1066 | */ |
| 1067 | void search_doc_touch(char cType, int rid, const char *zName){ |
| 1068 | if( search_index_exists() ){ |
| 1069 | db_multi_exec( |
| 1070 | "DELETE FROM ftsidx WHERE docid IN" |
| 1071 | " (SELECT id FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)", |
| 1072 | cType, rid |
| 1073 | ); |
| 1074 | db_multi_exec( |
| 1075 | "REPLACE INTO ftsdocs(type,rid,name,idxed)" |
| 1076 | " VALUES(%Q,%d,%Q,0)", |
| @@ -1107,11 +1126,11 @@ | |
| 1107 | " AND %z", |
| 1108 | ckid, glob_expr("foci.filename", db_get("doc-glob","")) |
| 1109 | ); |
| 1110 | db_multi_exec( |
| 1111 | "DELETE FROM ftsidx WHERE docid IN" |
| 1112 | " (SELECT id FROM ftsdocs WHERE type='d'" |
| 1113 | " AND rid NOT IN (SELECT rid FROM current_docs))" |
| 1114 | ); |
| 1115 | db_multi_exec( |
| 1116 | "DELETE FROM ftsdocs WHERE type='d'" |
| 1117 | " AND rid NOT IN (SELECT rid FROM current_docs)" |
| @@ -1125,11 +1144,11 @@ | |
| 1125 | " FROM current_docs", |
| 1126 | zBrUuid, rTime |
| 1127 | ); |
| 1128 | db_multi_exec( |
| 1129 | "INSERT INTO ftsidx(docid,stext)" |
| 1130 | " SELECT id, stext FROM ftscontent WHERE type='d' AND NOT idxed" |
| 1131 | ); |
| 1132 | db_multi_exec( |
| 1133 | "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed" |
| 1134 | ); |
| 1135 | } |
| @@ -1184,11 +1203,11 @@ | |
| 1184 | } |
| 1185 | case 2: { assert( fossil_strncmp(zSubCmd, "drop", n)==0 ); |
| 1186 | search_drop_index(); |
| 1187 | break; |
| 1188 | } |
| 1189 | case 3: { assert( fossil_strncmp(zSubCmd, "exist", n)==0 ); |
| 1190 | fossil_print("search_index_exists() = %d\n", search_index_exists()); |
| 1191 | break; |
| 1192 | } |
| 1193 | case 4: { assert( fossil_strncmp(zSubCmd, "fill", n)==0 ); |
| 1194 | search_fill_index(); |
| @@ -1201,11 +1220,11 @@ | |
| 1201 | break; |
| 1202 | } |
| 1203 | case 5: { assert( fossil_strncmp(zSubCmd, "pending", n)==0 ); |
| 1204 | Stmt q; |
| 1205 | if( !search_index_exists() ) break; |
| 1206 | db_prepare(&q, "SELECT id, type, rid, quote(label), url, date(mtime)" |
| 1207 | " FROM ftsdocs" |
| 1208 | " WHERE NOT idxed"); |
| 1209 | while( db_step(&q)==SQLITE_ROW ){ |
| 1210 | const char *zUrl = db_column_text(&q,4); |
| 1211 | if( zUrl && zUrl[0] ){ |
| @@ -1229,11 +1248,11 @@ | |
| 1229 | break; |
| 1230 | } |
| 1231 | case 6: { assert( fossil_strncmp(zSubCmd, "all", n)==0 ); |
| 1232 | Stmt q; |
| 1233 | if( !search_index_exists() ) break; |
| 1234 | db_prepare(&q, "SELECT id, type, rid, quote(name), idxed FROM ftsdocs"); |
| 1235 | while( db_step(&q)==SQLITE_ROW ){ |
| 1236 | fossil_print("%6d: %s %6d %s%s\n", |
| 1237 | db_column_int(&q, 0), |
| 1238 | db_column_text(&q, 1), |
| 1239 | db_column_int(&q, 2), |
| 1240 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -45,14 +45,15 @@ | |
| 45 | char *zPattern; /* The search pattern */ |
| 46 | char *zMarkBegin; /* Start of a match */ |
| 47 | char *zMarkEnd; /* End of a match */ |
| 48 | char *zMarkGap; /* A gap between two matches */ |
| 49 | unsigned fSrchFlg; /* Flags */ |
| 50 | int iScore; /* Score of the last match attempt */ |
| 51 | Blob snip; /* Snippet for the most recent match */ |
| 52 | }; |
| 53 | |
| 54 | #define SRCHFLG_HTML 0x01 /* Escape snippet text for HTML */ |
| 55 | #define SRCHFLG_STATIC 0x04 /* The static gSearch object */ |
| 56 | |
| 57 | #endif |
| 58 | |
| 59 | /* |
| @@ -92,10 +93,11 @@ | |
| 93 | if( p ){ |
| 94 | fossil_free(p->zPattern); |
| 95 | fossil_free(p->zMarkBegin); |
| 96 | fossil_free(p->zMarkEnd); |
| 97 | fossil_free(p->zMarkGap); |
| 98 | if( p->iScore ) blob_reset(&p->snip); |
| 99 | memset(p, 0, sizeof(*p)); |
| 100 | if( p!=&gSearch ) fossil_free(p); |
| 101 | } |
| 102 | } |
| 103 | |
| @@ -123,10 +125,11 @@ | |
| 125 | p->zPattern = z = mprintf("%s", zPattern); |
| 126 | p->zMarkBegin = mprintf("%s", zMarkBegin); |
| 127 | p->zMarkEnd = mprintf("%s", zMarkEnd); |
| 128 | p->zMarkGap = mprintf("%s", zMarkGap); |
| 129 | p->fSrchFlg = fSrchFlg; |
| 130 | blob_init(&p->snip, 0, 0); |
| 131 | while( *z && p->nTerm<SEARCH_MAX_TERM ){ |
| 132 | while( *z && !ISALNUM(*z) ){ z++; } |
| 133 | if( *z==0 ) break; |
| 134 | p->a[p->nTerm].z = z; |
| 135 | for(i=1; ISALNUM(z[i]); i++){} |
| @@ -156,24 +159,26 @@ | |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | /* |
| 163 | ** Compare a search pattern against one or more input strings which |
| 164 | ** collectively comprise a document. Return a match score. Any |
| 165 | ** postive value means there was a match. Zero means that one or |
| 166 | ** more terms are missing. |
| 167 | ** |
| 168 | ** The score and a snippet are record for future use. |
| 169 | ** |
| 170 | ** Scoring: |
| 171 | ** * All terms must match at least once or the score is zero |
| 172 | ** * One point for each matching term |
| 173 | ** * Extra points if consecutive words of the pattern are consecutive |
| 174 | ** in the document |
| 175 | */ |
| 176 | static int search_match( |
| 177 | Search *p, /* Search pattern and flags */ |
| 178 | int nDoc, /* Number of strings in this document */ |
| 179 | const char **azDoc /* Text of each string */ |
| 180 | ){ |
| 181 | int score; /* Final score */ |
| 182 | int i; /* Offset into current document */ |
| 183 | int ii; /* Loop counter */ |
| 184 | int j; /* Loop over search terms */ |
| @@ -226,18 +231,17 @@ | |
| 231 | /* Finished search all documents. |
| 232 | ** Every term must be seen or else the score is zero |
| 233 | */ |
| 234 | score = 1; |
| 235 | for(j=0; j<p->nTerm; j++) score *= anMatch[j]; |
| 236 | blob_reset(&p->snip); |
| 237 | p->iScore = score; |
| 238 | if( score==0 ) return score; |
| 239 | |
| 240 | |
| 241 | /* Prepare a snippet that describes the matching text. |
| 242 | */ |
| 243 | while(1){ |
| 244 | int iOfst; |
| 245 | int iTail; |
| 246 | int iBest; |
| 247 | for(ii=0; ii<p->nTerm && anMatch[ii]==0; ii++){} |
| @@ -272,11 +276,11 @@ | |
| 276 | if( iOfst<0 ) iOfst = 0; |
| 277 | while( iOfst>0 && ISALNUM(zDoc[iOfst-1]) ) iOfst--; |
| 278 | while( zDoc[iOfst] && !ISALNUM(zDoc[iOfst]) ) iOfst++; |
| 279 | for(ii=0; ii<CTX && zDoc[iTail]; ii++, iTail++){} |
| 280 | while( ISALNUM(zDoc[iTail]) ) iTail++; |
| 281 | if( iOfst>0 || wantGap ) blob_append(&p->snip, p->zMarkGap, -1); |
| 282 | wantGap = zDoc[iTail]!=0; |
| 283 | zDoc += iOfst; |
| 284 | iTail -= iOfst; |
| 285 | |
| 286 | /* Add a snippet segment using characters iOfst..iOfst+iTail from zDoc */ |
| @@ -285,53 +289,51 @@ | |
| 289 | for(j=0; j<p->nTerm; j++){ |
| 290 | int n = p->a[j].n; |
| 291 | if( sqlite3_strnicmp(p->a[j].z, &zDoc[i], n)==0 |
| 292 | && (!ISALNUM(zDoc[i+n]) || p->a[j].z[n]=='*') |
| 293 | ){ |
| 294 | snippet_text_append(p, &p->snip, zDoc, i); |
| 295 | zDoc += i; |
| 296 | iTail -= i; |
| 297 | blob_append(&p->snip, p->zMarkBegin, -1); |
| 298 | if( p->a[j].z[n]=='*' ){ |
| 299 | while( ISALNUM(zDoc[n]) ) n++; |
| 300 | } |
| 301 | snippet_text_append(p, &p->snip, zDoc, n); |
| 302 | zDoc += n; |
| 303 | iTail -= n; |
| 304 | blob_append(&p->snip, p->zMarkEnd, -1); |
| 305 | i = -1; |
| 306 | break; |
| 307 | } /* end-if */ |
| 308 | } /* end for(j) */ |
| 309 | if( j<p->nTerm ){ |
| 310 | while( ISALNUM(zDoc[i]) && i<iTail ){ i++; } |
| 311 | } |
| 312 | } /* end for(i) */ |
| 313 | snippet_text_append(p, &p->snip, zDoc, iTail); |
| 314 | } |
| 315 | if( wantGap ) blob_append(&p->snip, p->zMarkGap, -1); |
| 316 | return score; |
| 317 | } |
| 318 | |
| 319 | /* |
| 320 | ** COMMAND: test-match |
| 321 | ** |
| 322 | ** Usage: fossil test-match SEARCHSTRING FILE1 FILE2 ... |
| 323 | */ |
| 324 | void test_match_cmd(void){ |
| 325 | Search *p; |
| 326 | int i; |
| 327 | Blob x; |
| 328 | int score; |
| 329 | char *zDoc; |
| 330 | int flg = 0; |
| 331 | char *zBegin = (char*)find_option("begin",0,1); |
| 332 | char *zEnd = (char*)find_option("end",0,1); |
| 333 | char *zGap = (char*)find_option("gap",0,1); |
| 334 | if( find_option("html",0,0)!=0 ) flg |= SRCHFLG_HTML; |
| 335 | if( find_option("static",0,0)!=0 ) flg |= SRCHFLG_STATIC; |
| 336 | verify_all_options(); |
| 337 | if( g.argc<4 ) usage("SEARCHSTRING FILE1..."); |
| 338 | if( zBegin==0 ) zBegin = "[["; |
| 339 | if( zEnd==0 ) zEnd = "]]"; |
| @@ -338,18 +340,18 @@ | |
| 340 | if( zGap==0 ) zGap = " ... "; |
| 341 | p = search_init(g.argv[2], zBegin, zEnd, zGap, flg); |
| 342 | for(i=3; i<g.argc; i++){ |
| 343 | blob_read_from_file(&x, g.argv[i]); |
| 344 | zDoc = blob_str(&x); |
| 345 | score = search_match(p, 1, (const char**)&zDoc); |
| 346 | fossil_print("%s: %d\n", g.argv[i], p->iScore); |
| 347 | blob_reset(&x); |
| 348 | if( score ){ |
| 349 | fossil_print("%.78c\n%s\n%.78c\n\n", '=', blob_str(&p->snip), '='); |
| 350 | } |
| 351 | } |
| 352 | search_end(p); |
| 353 | } |
| 354 | |
| 355 | /* |
| 356 | ** An SQL function to initialize the global search pattern: |
| 357 | ** |
| @@ -385,35 +387,45 @@ | |
| 387 | search_end(&gSearch); |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | /* |
| 392 | ** Try to match the input text against the search parameters set up |
| 393 | ** by the previous search_init() call. Remember the results globally. |
| 394 | ** Return non-zero on a match and zero on a miss. |
| 395 | */ |
| 396 | static void search_match_sqlfunc( |
| 397 | sqlite3_context *context, |
| 398 | int argc, |
| 399 | sqlite3_value **argv |
| 400 | ){ |
| 401 | const char *zSText = (const char*)sqlite3_value_text(argv[0]); |
| 402 | int rc; |
| 403 | if( zSText==0 ) return; |
| 404 | rc = search_match(&gSearch, 1, &zSText); |
| 405 | sqlite3_result_int(context, rc); |
| 406 | } |
| 407 | |
| 408 | /* |
| 409 | ** These SQL functions return the results of the last |
| 410 | ** call to the search_match() SQL function. |
| 411 | */ |
| 412 | static void search_score_sqlfunc( |
| 413 | sqlite3_context *context, |
| 414 | int argc, |
| 415 | sqlite3_value **argv |
| 416 | ){ |
| 417 | sqlite3_result_int(context, gSearch.iScore); |
| 418 | } |
| 419 | static void search_snippet_sqlfunc( |
| 420 | sqlite3_context *context, |
| 421 | int argc, |
| 422 | sqlite3_value **argv |
| 423 | ){ |
| 424 | if( blob_size(&gSearch.snip)>0 ){ |
| 425 | sqlite3_result_text(context, blob_str(&gSearch.snip), -1, fossil_free); |
| 426 | blob_init(&gSearch.snip, 0, 0); |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | /* |
| 431 | ** This is an SQLite function that computes the searchable text. |
| @@ -451,14 +463,16 @@ | |
| 463 | ** do not delete the Search object. |
| 464 | */ |
| 465 | void search_sql_setup(sqlite3 *db){ |
| 466 | static int once = 0; |
| 467 | if( once++ ) return; |
| 468 | sqlite3_create_function(db, "search_match", 1, SQLITE_UTF8, 0, |
| 469 | search_match_sqlfunc, 0, 0); |
| 470 | sqlite3_create_function(db, "search_score", 0, SQLITE_UTF8, 0, |
| 471 | search_score_sqlfunc, 0, 0); |
| 472 | sqlite3_create_function(db, "search_snippet", 0, SQLITE_UTF8, 0, |
| 473 | search_snippet_sqlfunc, 0, 0); |
| 474 | sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0, |
| 475 | search_init_sqlfunc, 0, 0); |
| 476 | sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0, |
| 477 | search_stext_sqlfunc, 0, 0); |
| 478 | sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0, |
| @@ -587,28 +601,30 @@ | |
| 601 | static void search_fullscan( |
| 602 | const char *zPattern, /* The query pattern */ |
| 603 | unsigned int srchFlags /* What to search over */ |
| 604 | ){ |
| 605 | search_init(zPattern, "<b>", "</b>", " ... ", |
| 606 | SRCHFLG_STATIC|SRCHFLG_HTML); |
| 607 | if( (srchFlags & SRCH_DOC)!=0 ){ |
| 608 | char *zDocGlob = db_get("doc-glob",""); |
| 609 | char *zDocBr = db_get("doc-branch","trunk"); |
| 610 | if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){ |
| 611 | db_multi_exec( |
| 612 | "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 613 | ); |
| 614 | db_multi_exec( |
| 615 | "INSERT INTO x(label,url,score,date,snip)" |
| 616 | " SELECT printf('Document: %%s',foci.filename)," |
| 617 | " printf('%R/doc/%T/%%s',foci.filename)," |
| 618 | " search_score()," |
| 619 | " (SELECT datetime(event.mtime) FROM event" |
| 620 | " WHERE objid=symbolic_name_to_rid('trunk'))," |
| 621 | " search_snippet()" |
| 622 | " FROM foci CROSS JOIN blob" |
| 623 | " WHERE checkinID=symbolic_name_to_rid('trunk')" |
| 624 | " AND blob.uuid=foci.uuid" |
| 625 | " AND search_match(stext('d',blob.rid,foci.filename))" |
| 626 | " AND %z", |
| 627 | zDocBr, glob_expr("foci.filename", zDocGlob) |
| 628 | ); |
| 629 | } |
| 630 | } |
| @@ -619,16 +635,18 @@ | |
| 635 | " FROM tag, tagxref" |
| 636 | " WHERE tag.tagname GLOB 'wiki-*'" |
| 637 | " AND tagxref.tagid=tag.tagid" |
| 638 | " GROUP BY 1" |
| 639 | ")" |
| 640 | "INSERT INTO x(label,url,score,date,snip)" |
| 641 | " SELECT printf('Wiki: %%s',name)," |
| 642 | " printf('%R/wiki?name=%%s',urlencode(name))," |
| 643 | " search_score()," |
| 644 | " datetime(mtime)," |
| 645 | " search_snippet()" |
| 646 | " FROM wiki" |
| 647 | " WHERE search_match(stext('w',rid,name));" |
| 648 | ); |
| 649 | } |
| 650 | if( (srchFlags & SRCH_CKIN)!=0 ){ |
| 651 | db_multi_exec( |
| 652 | "WITH ckin(uuid,rid,mtime) AS (" |
| @@ -635,32 +653,33 @@ | |
| 653 | " SELECT blob.uuid, event.objid, event.mtime" |
| 654 | " FROM event, blob" |
| 655 | " WHERE event.type='ci'" |
| 656 | " AND blob.rid=event.objid" |
| 657 | ")" |
| 658 | "INSERT INTO x(label,url,score,date,snip)" |
| 659 | " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime))," |
| 660 | " printf('%R/timeline?c=%%s&n=8&y=ci',uuid)," |
| 661 | " search_score()," |
| 662 | " datetime(mtime)," |
| 663 | " search_snippet()" |
| 664 | " FROM ckin" |
| 665 | " WHERE search_match(stext('c',rid,NULL));" |
| 666 | ); |
| 667 | } |
| 668 | if( (srchFlags & SRCH_TKT)!=0 ){ |
| 669 | db_multi_exec( |
| 670 | "INSERT INTO x(label,url,score, date,snip)" |
| 671 | " SELECT printf('Ticket [%%.17s] on %%s'," |
| 672 | "tkt_uuid,datetime(tkt_mtime))," |
| 673 | " printf('%R/tktview/%%.20s',tkt_uuid)," |
| 674 | " search_score()," |
| 675 | " datetime(tkt_mtime)," |
| 676 | " search_snippet()" |
| 677 | " FROM ticket" |
| 678 | " WHERE search_match(stext('t',tkt_id,NULL));" |
| 679 | ); |
| 680 | } |
| 681 | } |
| 682 | |
| 683 | /* |
| 684 | ** When this routine is called, there already exists a table |
| 685 | ** |
| @@ -679,14 +698,14 @@ | |
| 698 | "INSERT INTO x(label,url,score,date,snip) " |
| 699 | " SELECT ftsdocs.label," |
| 700 | " ftsdocs.url," |
| 701 | " 1," /*FIX ME*/ |
| 702 | " datetime(ftsdocs.mtime)," |
| 703 | " snippet(ftsidx)" |
| 704 | " FROM ftsidx, ftsdocs" |
| 705 | " WHERE ftsidx MATCH %Q" |
| 706 | " AND ftsdocs.rowid=ftsidx.docid", |
| 707 | zPattern |
| 708 | ); |
| 709 | } |
| 710 | |
| 711 | |
| @@ -981,11 +1000,11 @@ | |
| 1000 | /* The schema for the full-text index |
| 1001 | */ |
| 1002 | static const char zFtsSchema[] = |
| 1003 | @ -- One entry for each possible search result |
| 1004 | @ CREATE TABLE IF NOT EXISTS "%w".ftsdocs( |
| 1005 | @ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid |
| 1006 | @ type CHAR(1), -- Type of document |
| 1007 | @ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document |
| 1008 | @ name TEXT, -- Additional document description |
| 1009 | @ idxed BOOLEAN, -- True if currently in the index |
| 1010 | @ label TEXT, -- Label to print on search results |
| @@ -993,11 +1012,11 @@ | |
| 1012 | @ mtime DATE, -- Date when document created |
| 1013 | @ UNIQUE(type,rid) |
| 1014 | @ ); |
| 1015 | @ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0; |
| 1016 | @ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS |
| 1017 | @ SELECT rowid, type, rid, name, idxed, label, url, mtime, |
| 1018 | @ stext(type,rid,name) AS 'stext' |
| 1019 | @ FROM ftsdocs; |
| 1020 | @ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx |
| 1021 | @ USING fts4(content="ftscontent", stext); |
| 1022 | ; |
| @@ -1066,11 +1085,11 @@ | |
| 1085 | */ |
| 1086 | void search_doc_touch(char cType, int rid, const char *zName){ |
| 1087 | if( search_index_exists() ){ |
| 1088 | db_multi_exec( |
| 1089 | "DELETE FROM ftsidx WHERE docid IN" |
| 1090 | " (SELECT rowid FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)", |
| 1091 | cType, rid |
| 1092 | ); |
| 1093 | db_multi_exec( |
| 1094 | "REPLACE INTO ftsdocs(type,rid,name,idxed)" |
| 1095 | " VALUES(%Q,%d,%Q,0)", |
| @@ -1107,11 +1126,11 @@ | |
| 1126 | " AND %z", |
| 1127 | ckid, glob_expr("foci.filename", db_get("doc-glob","")) |
| 1128 | ); |
| 1129 | db_multi_exec( |
| 1130 | "DELETE FROM ftsidx WHERE docid IN" |
| 1131 | " (SELECT rowid FROM ftsdocs WHERE type='d'" |
| 1132 | " AND rid NOT IN (SELECT rid FROM current_docs))" |
| 1133 | ); |
| 1134 | db_multi_exec( |
| 1135 | "DELETE FROM ftsdocs WHERE type='d'" |
| 1136 | " AND rid NOT IN (SELECT rid FROM current_docs)" |
| @@ -1125,11 +1144,11 @@ | |
| 1144 | " FROM current_docs", |
| 1145 | zBrUuid, rTime |
| 1146 | ); |
| 1147 | db_multi_exec( |
| 1148 | "INSERT INTO ftsidx(docid,stext)" |
| 1149 | " SELECT rowid, stext FROM ftscontent WHERE type='d' AND NOT idxed" |
| 1150 | ); |
| 1151 | db_multi_exec( |
| 1152 | "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed" |
| 1153 | ); |
| 1154 | } |
| @@ -1184,11 +1203,11 @@ | |
| 1203 | } |
| 1204 | case 2: { assert( fossil_strncmp(zSubCmd, "drop", n)==0 ); |
| 1205 | search_drop_index(); |
| 1206 | break; |
| 1207 | } |
| 1208 | case 3: { assert( fossil_strncmp(zSubCmd, "exists", n)==0 ); |
| 1209 | fossil_print("search_index_exists() = %d\n", search_index_exists()); |
| 1210 | break; |
| 1211 | } |
| 1212 | case 4: { assert( fossil_strncmp(zSubCmd, "fill", n)==0 ); |
| 1213 | search_fill_index(); |
| @@ -1201,11 +1220,11 @@ | |
| 1220 | break; |
| 1221 | } |
| 1222 | case 5: { assert( fossil_strncmp(zSubCmd, "pending", n)==0 ); |
| 1223 | Stmt q; |
| 1224 | if( !search_index_exists() ) break; |
| 1225 | db_prepare(&q, "SELECT rowid, type, rid, quote(label), url, date(mtime)" |
| 1226 | " FROM ftsdocs" |
| 1227 | " WHERE NOT idxed"); |
| 1228 | while( db_step(&q)==SQLITE_ROW ){ |
| 1229 | const char *zUrl = db_column_text(&q,4); |
| 1230 | if( zUrl && zUrl[0] ){ |
| @@ -1229,11 +1248,11 @@ | |
| 1248 | break; |
| 1249 | } |
| 1250 | case 6: { assert( fossil_strncmp(zSubCmd, "all", n)==0 ); |
| 1251 | Stmt q; |
| 1252 | if( !search_index_exists() ) break; |
| 1253 | db_prepare(&q, "SELECT rowid,type,rid,quote(name),idxed FROM ftsdocs"); |
| 1254 | while( db_step(&q)==SQLITE_ROW ){ |
| 1255 | fossil_print("%6d: %s %6d %s%s\n", |
| 1256 | db_column_int(&q, 0), |
| 1257 | db_column_text(&q, 1), |
| 1258 | db_column_int(&q, 2), |
| 1259 |