| | @@ -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 | ** |
| | @@ -361,12 +363,12 @@ |
| 361 | 363 | sqlite3_context *context, |
| 362 | 364 | int argc, |
| 363 | 365 | sqlite3_value **argv |
| 364 | 366 | ){ |
| 365 | 367 | const char *zPattern = 0; |
| 366 | | - const char *zBegin = "<b>"; |
| 367 | | - const char *zEnd = "</b>"; |
| 368 | + const char *zBegin = "<mark>"; |
| 369 | + const char *zEnd = "</mark>"; |
| 368 | 370 | const char *zGap = " ... "; |
| 369 | 371 | unsigned int flg = SRCHFLG_HTML; |
| 370 | 372 | switch( argc ){ |
| 371 | 373 | default: |
| 372 | 374 | flg = (unsigned int)sqlite3_value_int(argv[4]); |
| | @@ -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. |
| | @@ -449,14 +461,18 @@ |
| 449 | 461 | ** Register the "score()" SQL function to score its input text |
| 450 | 462 | ** using the given Search object. Once this function is registered, |
| 451 | 463 | ** do not delete the Search object. |
| 452 | 464 | */ |
| 453 | 465 | void search_sql_setup(sqlite3 *db){ |
| 454 | | - sqlite3_create_function(db, "score", -1, SQLITE_UTF8, 0, |
| 455 | | - search_score_sqlfunc, 0, 0); |
| 456 | | - sqlite3_create_function(db, "snippet", -1, SQLITE_UTF8, &gSearch, |
| 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, |
| 457 | 471 | search_score_sqlfunc, 0, 0); |
| 472 | + sqlite3_create_function(db, "search_snippet", 0, SQLITE_UTF8, 0, |
| 473 | + search_snippet_sqlfunc, 0, 0); |
| 458 | 474 | sqlite3_create_function(db, "search_init", -1, SQLITE_UTF8, 0, |
| 459 | 475 | search_init_sqlfunc, 0, 0); |
| 460 | 476 | sqlite3_create_function(db, "stext", 3, SQLITE_UTF8, 0, |
| 461 | 477 | search_stext_sqlfunc, 0, 0); |
| 462 | 478 | sqlite3_create_function(db, "urlencode", 1, SQLITE_UTF8, 0, |
| | @@ -551,28 +567,217 @@ |
| 551 | 567 | /* |
| 552 | 568 | ** Remove bits from srchFlags which are disallowed by either the |
| 553 | 569 | ** current server configuration or by user permissions. |
| 554 | 570 | */ |
| 555 | 571 | unsigned int search_restrict(unsigned int srchFlags){ |
| 556 | | - if( (srchFlags & SRCH_CKIN)!=0 |
| 557 | | - && (g.perm.Read==0 || db_get_boolean("search-ci",0)==0) ){ |
| 572 | + if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC); |
| 573 | + if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT); |
| 574 | + if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI); |
| 575 | + if( search_index_exists() ) return srchFlags; |
| 576 | + if( (srchFlags & SRCH_CKIN)!=0 && db_get_boolean("search-ci",0)==0 ){ |
| 558 | 577 | srchFlags &= ~SRCH_CKIN; |
| 559 | 578 | } |
| 560 | | - if( (srchFlags & SRCH_DOC)!=0 |
| 561 | | - && (g.perm.Read==0 || db_get_boolean("search-doc",0)==0) ){ |
| 579 | + if( (srchFlags & SRCH_DOC)!=0 && db_get_boolean("search-doc",0)==0 ){ |
| 562 | 580 | srchFlags &= ~SRCH_DOC; |
| 563 | 581 | } |
| 564 | | - if( (srchFlags & SRCH_TKT)!=0 |
| 565 | | - && (g.perm.RdTkt==0 || db_get_boolean("search-tkt",0)==0) ){ |
| 582 | + if( (srchFlags & SRCH_TKT)!=0 && db_get_boolean("search-tkt",0)==0 ){ |
| 566 | 583 | srchFlags &= ~SRCH_TKT; |
| 567 | 584 | } |
| 568 | | - if( (srchFlags & SRCH_WIKI)!=0 |
| 569 | | - && (g.perm.RdWiki==0 || db_get_boolean("search-wiki",0)==0) ){ |
| 585 | + if( (srchFlags & SRCH_WIKI)!=0 && db_get_boolean("search-wiki",0)==0 ){ |
| 570 | 586 | srchFlags &= ~SRCH_WIKI; |
| 571 | 587 | } |
| 572 | 588 | return srchFlags; |
| 573 | 589 | } |
| 590 | + |
| 591 | +/* |
| 592 | +** When this routine is called, there already exists a table |
| 593 | +** |
| 594 | +** x(label,url,score,date,snip). |
| 595 | +** |
| 596 | +** And the srchFlags parameter has been validated. This routine |
| 597 | +** fills the X table with search results using a full-text scan. |
| 598 | +** |
| 599 | +** The companion indexed scan routine is search_indexed(). |
| 600 | +*/ |
| 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 | + } |
| 631 | + if( (srchFlags & SRCH_WIKI)!=0 ){ |
| 632 | + db_multi_exec( |
| 633 | + "WITH wiki(name,rid,mtime) AS (" |
| 634 | + " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)" |
| 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 (" |
| 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 | +** Implemenation of the rank() function used with rank(matchinfo(*,'pcsx')). |
| 685 | +*/ |
| 686 | +static void search_rank_sqlfunc( |
| 687 | + sqlite3_context *context, |
| 688 | + int argc, |
| 689 | + sqlite3_value **argv |
| 690 | +){ |
| 691 | + const unsigned *aVal = (unsigned int*)sqlite3_value_blob(argv[0]); |
| 692 | + int nVal = sqlite3_value_bytes(argv[0])/4; |
| 693 | + int nTerm; /* Number of search terms in the query */ |
| 694 | + int i; /* Loop counter */ |
| 695 | + double r = 1.0; /* Score */ |
| 696 | + |
| 697 | + if( nVal<6 ) return; |
| 698 | + if( aVal[1]!=1 ) return; |
| 699 | + nTerm = aVal[0]; |
| 700 | + r *= 1<<((30*(aVal[2]-1))/nTerm); |
| 701 | + for(i=1; i<=nTerm; i++){ |
| 702 | + int hits_this_row = aVal[3*i]; |
| 703 | + int hits_all_rows = aVal[3*i+1]; |
| 704 | + int rows_with_hit = aVal[3*i+2]; |
| 705 | + double avg_hits_per_row = (double)hits_all_rows/(double)rows_with_hit; |
| 706 | + r *= hits_this_row/avg_hits_per_row; |
| 707 | + } |
| 708 | +#define SEARCH_DEBUG_RANK 0 |
| 709 | +#if SEARCH_DEBUG_RANK |
| 710 | + { |
| 711 | + Blob x; |
| 712 | + blob_init(&x,0,0); |
| 713 | + blob_appendf(&x,"%08x", (int)r); |
| 714 | + for(i=0; i<nVal; i++){ |
| 715 | + blob_appendf(&x," %d", aVal[i]); |
| 716 | + } |
| 717 | + blob_appendf(&x," r=%g", r); |
| 718 | + sqlite3_result_text(context, blob_str(&x), -1, fossil_free); |
| 719 | + } |
| 720 | +#else |
| 721 | + sqlite3_result_double(context, r); |
| 722 | +#endif |
| 723 | +} |
| 724 | + |
| 725 | +/* |
| 726 | +** When this routine is called, there already exists a table |
| 727 | +** |
| 728 | +** x(label,url,score,date,snip). |
| 729 | +** |
| 730 | +** And the srchFlags parameter has been validated. This routine |
| 731 | +** fills the X table with search results using a index scan. |
| 732 | +** |
| 733 | +** The companion full-text scan routine is search_fullscan(). |
| 734 | +*/ |
| 735 | +static void search_indexed( |
| 736 | + const char *zPattern, /* The query pattern */ |
| 737 | + unsigned int srchFlags /* What to search over */ |
| 738 | +){ |
| 739 | + Blob sql; |
| 740 | + if( srchFlags==0 ) return; |
| 741 | + sqlite3_create_function(g.db, "rank", 1, SQLITE_UTF8, 0, |
| 742 | + search_rank_sqlfunc, 0, 0); |
| 743 | + blob_init(&sql, 0, 0); |
| 744 | + blob_appendf(&sql, |
| 745 | + "INSERT INTO x(label,url,score,date,snip) " |
| 746 | + " SELECT ftsdocs.label," |
| 747 | + " ftsdocs.url," |
| 748 | + " rank(matchinfo(ftsidx,'pcsx'))," |
| 749 | + " datetime(ftsdocs.mtime)," |
| 750 | + " snippet(ftsidx,'<mark>','</mark>')" |
| 751 | + " FROM ftsidx, ftsdocs" |
| 752 | + " WHERE ftsidx MATCH %Q" |
| 753 | + " AND ftsdocs.rowid=ftsidx.docid", |
| 754 | + zPattern |
| 755 | + ); |
| 756 | + if( srchFlags!=SRCH_ALL ){ |
| 757 | + const char *zSep = " AND ("; |
| 758 | + static const struct { unsigned m; char c; } aMask[] = { |
| 759 | + { SRCH_CKIN, 'c' }, |
| 760 | + { SRCH_DOC, 'd' }, |
| 761 | + { SRCH_TKT, 't' }, |
| 762 | + { SRCH_WIKI, 'w' }, |
| 763 | + }; |
| 764 | + int i; |
| 765 | + for(i=0; i<ArraySize(aMask); i++){ |
| 766 | + if( srchFlags & aMask[i].m ){ |
| 767 | + blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c); |
| 768 | + zSep = " OR "; |
| 769 | + } |
| 770 | + } |
| 771 | + blob_append(&sql,")",1); |
| 772 | + } |
| 773 | + db_multi_exec("%s",blob_str(&sql)/*safe-for-%s*/); |
| 774 | +#if SEARCH_DEBUG_RANK |
| 775 | + db_multi_exec("UPDATE x SET label=printf('%%s (score=%%s)',label,score)"); |
| 776 | +#endif |
| 777 | +} |
| 778 | + |
| 574 | 779 | |
| 575 | 780 | /* |
| 576 | 781 | ** This routine generates web-page output for a search operation. |
| 577 | 782 | ** Other web-pages can invoke this routine to add search results |
| 578 | 783 | ** in the middle of the page. |
| | @@ -588,91 +793,32 @@ |
| 588 | 793 | |
| 589 | 794 | srchFlags = search_restrict(srchFlags); |
| 590 | 795 | if( srchFlags==0 ) return 0; |
| 591 | 796 | search_sql_setup(g.db); |
| 592 | 797 | add_content_sql_commands(g.db); |
| 593 | | - search_init(zPattern, "<b>", "</b>", " ... ", |
| 594 | | - SRCHFLG_STATIC|SRCHFLG_HTML|SRCHFLG_SCORE); |
| 595 | | - db_multi_exec( |
| 596 | | - "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 597 | | - "CREATE TEMP TABLE x(label TEXT,url TEXT,date TEXT,snip TEXT);" |
| 598 | | - ); |
| 599 | | - if( (srchFlags & SRCH_DOC)!=0 ){ |
| 600 | | - char *zDocGlob = db_get("doc-glob",""); |
| 601 | | - char *zDocBr = db_get("doc-branch","trunk"); |
| 602 | | - if( zDocGlob && zDocGlob[0] && zDocBr && zDocBr[0] ){ |
| 603 | | - db_multi_exec( |
| 604 | | - "INSERT INTO x(label,url,date,snip)" |
| 605 | | - " SELECT printf('Document: %%s',foci.filename)," |
| 606 | | - " printf('%R/doc/%T/%%s',foci.filename)," |
| 607 | | - " (SELECT datetime(event.mtime) FROM event" |
| 608 | | - " WHERE objid=symbolic_name_to_rid('trunk'))," |
| 609 | | - " snippet(stext('d',blob.rid,foci.filename))" |
| 610 | | - " FROM foci CROSS JOIN blob" |
| 611 | | - " WHERE checkinID=symbolic_name_to_rid('trunk')" |
| 612 | | - " AND blob.uuid=foci.uuid" |
| 613 | | - " AND %z", |
| 614 | | - zDocBr, glob_expr("foci.filename", zDocGlob) |
| 615 | | - ); |
| 616 | | - } |
| 617 | | - } |
| 618 | | - if( (srchFlags & SRCH_WIKI)!=0 ){ |
| 619 | | - db_multi_exec( |
| 620 | | - "WITH wiki(name,rid,mtime) AS (" |
| 621 | | - " SELECT substr(tagname,6), tagxref.rid, max(tagxref.mtime)" |
| 622 | | - " FROM tag, tagxref" |
| 623 | | - " WHERE tag.tagname GLOB 'wiki-*'" |
| 624 | | - " AND tagxref.tagid=tag.tagid" |
| 625 | | - " GROUP BY 1" |
| 626 | | - ")" |
| 627 | | - "INSERT INTO x(label,url,date,snip)" |
| 628 | | - " SELECT printf('Wiki: %%s',name)," |
| 629 | | - " printf('%R/wiki?name=%%s',urlencode(name))," |
| 630 | | - " datetime(mtime)," |
| 631 | | - " snippet(stext('w',rid,name))" |
| 632 | | - " FROM wiki;" |
| 633 | | - ); |
| 634 | | - } |
| 635 | | - if( (srchFlags & SRCH_CKIN)!=0 ){ |
| 636 | | - db_multi_exec( |
| 637 | | - "WITH ckin(uuid,rid,mtime) AS (" |
| 638 | | - " SELECT blob.uuid, event.objid, event.mtime" |
| 639 | | - " FROM event, blob" |
| 640 | | - " WHERE event.type='ci'" |
| 641 | | - " AND blob.rid=event.objid" |
| 642 | | - ")" |
| 643 | | - "INSERT INTO x(label,url,date,snip)" |
| 644 | | - " SELECT printf('Check-in [%%.10s] on %%s',uuid,datetime(mtime))," |
| 645 | | - " printf('%R/timeline?c=%%s&n=8&y=ci',uuid)," |
| 646 | | - " datetime(mtime)," |
| 647 | | - " snippet(stext('c',rid,NULL))" |
| 648 | | - " FROM ckin;" |
| 649 | | - ); |
| 650 | | - } |
| 651 | | - if( (srchFlags & SRCH_TKT)!=0 ){ |
| 652 | | - db_multi_exec( |
| 653 | | - "INSERT INTO x(label,url,date,snip)" |
| 654 | | - " SELECT printf('Ticket [%%.17s] on %%s'," |
| 655 | | - "tkt_uuid,datetime(tkt_mtime))," |
| 656 | | - " printf('%R/tktview/%%.20s',tkt_uuid)," |
| 657 | | - " datetime(tkt_mtime)," |
| 658 | | - " snippet(stext('t',tkt_id,NULL))" |
| 659 | | - " FROM ticket;" |
| 660 | | - ); |
| 661 | | - } |
| 662 | | - db_prepare(&q, "SELECT url, substr(snip,9), label" |
| 663 | | - " FROM x WHERE snip IS NOT NULL" |
| 664 | | - " ORDER BY substr(snip,1,8) DESC, date DESC;"); |
| 798 | + db_multi_exec( |
| 799 | + "CREATE TEMP TABLE x(label,url,score,date,snip);" |
| 800 | + ); |
| 801 | + if( !search_index_exists() ){ |
| 802 | + search_fullscan(zPattern, srchFlags); |
| 803 | + }else{ |
| 804 | + search_update_index(srchFlags); |
| 805 | + search_indexed(zPattern, srchFlags); |
| 806 | + } |
| 807 | + db_prepare(&q, "SELECT url, snip, label" |
| 808 | + " FROM x" |
| 809 | + " ORDER BY score DESC, date DESC;"); |
| 665 | 810 | while( db_step(&q)==SQLITE_ROW ){ |
| 666 | 811 | const char *zUrl = db_column_text(&q, 0); |
| 667 | 812 | const char *zSnippet = db_column_text(&q, 1); |
| 668 | 813 | const char *zLabel = db_column_text(&q, 2); |
| 669 | 814 | if( nRow==0 ){ |
| 670 | 815 | @ <ol> |
| 671 | 816 | } |
| 672 | 817 | nRow++; |
| 673 | | - @ <li><p><a href='%s(zUrl)'>%h(zLabel)</a><br>%s(zSnippet)</li> |
| 818 | + @ <li><p><a href='%s(zUrl)'>%h(zLabel)</a><br> |
| 819 | + @ <span class='snippet'>%s(zSnippet)</span></li> |
| 674 | 820 | } |
| 675 | 821 | db_finalize(&q); |
| 676 | 822 | if( nRow ){ |
| 677 | 823 | @ </ol> |
| 678 | 824 | } |
| | @@ -789,18 +935,18 @@ |
| 789 | 935 | ** zName Name of the object being searched. |
| 790 | 936 | */ |
| 791 | 937 | void search_stext( |
| 792 | 938 | char cType, /* Type of document */ |
| 793 | 939 | int rid, /* BLOB.RID or TAG.TAGID value for document */ |
| 794 | | - const char *zName, /* Name of the document */ |
| 940 | + const char *zName, /* Auxiliary information */ |
| 795 | 941 | Blob *pOut /* OUT: Initialize to the search text */ |
| 796 | 942 | ){ |
| 797 | 943 | blob_init(pOut, 0, 0); |
| 798 | 944 | switch( cType ){ |
| 799 | 945 | case 'd': { /* Documents */ |
| 800 | 946 | Blob doc; |
| 801 | | - content_get(rid, &doc); |
| 947 | + content_get(rid, &doc); |
| 802 | 948 | blob_to_utf8_no_bom(&doc, 0); |
| 803 | 949 | get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut); |
| 804 | 950 | blob_reset(&doc); |
| 805 | 951 | break; |
| 806 | 952 | } |
| | @@ -860,63 +1006,355 @@ |
| 860 | 1006 | break; |
| 861 | 1007 | } |
| 862 | 1008 | } |
| 863 | 1009 | } |
| 864 | 1010 | |
| 865 | | -/* |
| 866 | | -** The arguments cType,rid,zName define an object that can be searched |
| 867 | | -** for. Return a URL (relative to the root of the Fossil project) that |
| 868 | | -** will jump to that document. |
| 869 | | -** |
| 870 | | -** Space to hold the returned string is obtained from mprintf() and should |
| 871 | | -** be freed by the caller using fossil_free() or the equivalent. |
| 872 | | -*/ |
| 873 | | -char *search_url( |
| 874 | | - char cType, /* Type of document */ |
| 875 | | - int rid, /* BLOB.RID or TAG.TAGID for the object */ |
| 876 | | - const char *zName /* Name of the object */ |
| 877 | | -){ |
| 878 | | - char *zUrl = 0; |
| 879 | | - switch( cType ){ |
| 880 | | - case 'd': { /* Documents */ |
| 881 | | - zUrl = db_text(0, |
| 882 | | - "SELECT printf('/doc/%%s%%s', substr(blob.uuid,20), %Q)" |
| 883 | | - " FROM mlink, blob" |
| 884 | | - " WHERE mlink.fid=%d AND mlink.mid=blob.rid", |
| 885 | | - zName, rid); |
| 886 | | - break; |
| 887 | | - } |
| 888 | | - case 'w': { /* Wiki */ |
| 889 | | - char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 890 | | - zUrl = mprintf("/wiki?id=%z&name=%t", zId, zName); |
| 891 | | - break; |
| 892 | | - } |
| 893 | | - case 'c': { /* Ckeck-in Comment */ |
| 894 | | - char *zId = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 895 | | - zUrl = mprintf("/info/%z", zId); |
| 896 | | - break; |
| 897 | | - } |
| 898 | | - case 't': { /* Tickets */ |
| 899 | | - char *zId = db_text(0, "SELECT tkt_uuid FROM ticket" |
| 900 | | - " WHERE tkt_id=%d", rid); |
| 901 | | - zUrl = mprintf("/tktview/%.20z", zId); |
| 902 | | - break; |
| 903 | | - } |
| 904 | | - } |
| 905 | | - return zUrl; |
| 906 | | -} |
| 907 | | - |
| 908 | 1011 | /* |
| 909 | 1012 | ** COMMAND: test-search-stext |
| 910 | 1013 | ** |
| 911 | 1014 | ** Usage: fossil test-search-stext TYPE ARG1 ARG2 |
| 912 | 1015 | */ |
| 913 | 1016 | void test_search_stext(void){ |
| 914 | 1017 | Blob out; |
| 915 | | - char *zUrl; |
| 916 | 1018 | db_find_and_open_repository(0,0); |
| 917 | 1019 | if( g.argc!=5 ) usage("TYPE RID NAME"); |
| 918 | 1020 | search_stext(g.argv[2][0], atoi(g.argv[3]), g.argv[4], &out); |
| 919 | | - zUrl = search_url(g.argv[2][0], atoi(g.argv[3]), g.argv[4]); |
| 920 | | - fossil_print("%s\n%z\n",blob_str(&out),zUrl); |
| 1021 | + fossil_print("%s\n",blob_str(&out)); |
| 921 | 1022 | blob_reset(&out); |
| 922 | 1023 | } |
| 1024 | + |
| 1025 | +/* The schema for the full-text index |
| 1026 | +*/ |
| 1027 | +static const char zFtsSchema[] = |
| 1028 | +@ -- One entry for each possible search result |
| 1029 | +@ CREATE TABLE IF NOT EXISTS "%w".ftsdocs( |
| 1030 | +@ rowid INTEGER PRIMARY KEY, -- Maps to the ftsidx.docid |
| 1031 | +@ type CHAR(1), -- Type of document |
| 1032 | +@ rid INTEGER, -- BLOB.RID or TAG.TAGID for the document |
| 1033 | +@ name TEXT, -- Additional document description |
| 1034 | +@ idxed BOOLEAN, -- True if currently in the index |
| 1035 | +@ label TEXT, -- Label to print on search results |
| 1036 | +@ url TEXT, -- URL to access this document |
| 1037 | +@ mtime DATE, -- Date when document created |
| 1038 | +@ UNIQUE(type,rid) |
| 1039 | +@ ); |
| 1040 | +@ CREATE INDEX "%w".ftsdocIdxed ON ftsdocs(type,rid,name) WHERE idxed==0; |
| 1041 | +@ CREATE INDEX "%w".ftsdocName ON ftsdocs(name) WHERE type='w'; |
| 1042 | +@ CREATE VIEW IF NOT EXISTS "%w".ftscontent AS |
| 1043 | +@ SELECT rowid, type, rid, name, idxed, label, url, mtime, |
| 1044 | +@ stext(type,rid,name) AS 'stext' |
| 1045 | +@ FROM ftsdocs; |
| 1046 | +@ CREATE VIRTUAL TABLE IF NOT EXISTS "%w".ftsidx |
| 1047 | +@ USING fts4(content="ftscontent", stext); |
| 1048 | +; |
| 1049 | +static const char zFtsDrop[] = |
| 1050 | +@ DROP TABLE IF EXISTS "%w".ftsidx; |
| 1051 | +@ DROP VIEW IF EXISTS "%w".ftscontent; |
| 1052 | +@ DROP TABLE IF EXISTS "%w".ftsdocs; |
| 1053 | +; |
| 1054 | + |
| 1055 | +/* |
| 1056 | +** Create or drop the tables associated with a full-text index. |
| 1057 | +*/ |
| 1058 | +void search_create_index(void){ |
| 1059 | + const char *zDb = db_name("repository"); |
| 1060 | + search_sql_setup(g.db); |
| 1061 | + db_multi_exec(zFtsSchema/*works-like:"%w%w%w%w%w"*/, |
| 1062 | + zDb, zDb, zDb, zDb, zDb); |
| 1063 | +} |
| 1064 | +void search_drop_index(void){ |
| 1065 | + const char *zDb = db_name("repository"); |
| 1066 | + db_multi_exec(zFtsDrop/*works-like:"%w%w%w"*/, zDb, zDb, zDb); |
| 1067 | +} |
| 1068 | + |
| 1069 | +/* |
| 1070 | +** Return true if the full-text search index exists |
| 1071 | +*/ |
| 1072 | +int search_index_exists(void){ |
| 1073 | + static int fExists = -1; |
| 1074 | + if( fExists<0 ) fExists = db_table_exists("repository","ftsdocs"); |
| 1075 | + return fExists; |
| 1076 | +} |
| 1077 | + |
| 1078 | +/* |
| 1079 | +** Fill the FTSDOCS table with unindexed entries for everything |
| 1080 | +** in the repository. This uses INSERT OR IGNORE so entries already |
| 1081 | +** in FTSDOCS are unchanged. |
| 1082 | +*/ |
| 1083 | +void search_fill_index(void){ |
| 1084 | + if( !search_index_exists() ) return; |
| 1085 | + search_sql_setup(g.db); |
| 1086 | + db_multi_exec( |
| 1087 | + "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)" |
| 1088 | + " SELECT 'c', objid, 0 FROM event WHERE type='ci';" |
| 1089 | + ); |
| 1090 | + db_multi_exec( |
| 1091 | + "WITH latest_wiki(rid,name,mtime) AS (" |
| 1092 | + " SELECT tagxref.rid, substr(tag.tagname,6), max(tagxref.mtime)" |
| 1093 | + " FROM tag, tagxref" |
| 1094 | + " WHERE tag.tagname GLOB 'wiki-*'" |
| 1095 | + " AND tagxref.tagid=tag.tagid" |
| 1096 | + " AND tagxref.value>0" |
| 1097 | + " GROUP BY 2" |
| 1098 | + ") INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)" |
| 1099 | + " SELECT 'w', rid, name, 0 FROM latest_wiki;" |
| 1100 | + ); |
| 1101 | + db_multi_exec( |
| 1102 | + "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)" |
| 1103 | + " SELECT 't', tkt_id, 0 FROM ticket;" |
| 1104 | + ); |
| 1105 | +} |
| 1106 | + |
| 1107 | +/* |
| 1108 | +** The document described by cType,rid,zName is about to be added or |
| 1109 | +** updated. If the document has already been indexed, then unindex it |
| 1110 | +** now while we still have access to the old content. Add the document |
| 1111 | +** to the queue of documents that need to be indexed or reindexed. |
| 1112 | +*/ |
| 1113 | +void search_doc_touch(char cType, int rid, const char *zName){ |
| 1114 | + if( search_index_exists() ){ |
| 1115 | + char zType[2]; |
| 1116 | + zType[0] = cType; |
| 1117 | + zType[1] = 0; |
| 1118 | + db_multi_exec( |
| 1119 | + "DELETE FROM ftsidx WHERE docid IN" |
| 1120 | + " (SELECT rowid FROM ftsdocs WHERE type=%Q AND rid=%d AND idxed)", |
| 1121 | + zType, rid |
| 1122 | + ); |
| 1123 | + db_multi_exec( |
| 1124 | + "REPLACE INTO ftsdocs(type,rid,name,idxed)" |
| 1125 | + " VALUES(%Q,%d,%Q,0)", |
| 1126 | + zType, rid, zName |
| 1127 | + ); |
| 1128 | + if( cType=='w' ){ |
| 1129 | + db_multi_exec( |
| 1130 | + "DELETE FROM ftsidx WHERE docid IN" |
| 1131 | + " (SELECT rowid FROM ftsdocs WHERE type='w' AND name=%Q AND idxed)", |
| 1132 | + zName |
| 1133 | + ); |
| 1134 | + db_multi_exec( |
| 1135 | + "DELETE FROM ftsdocs WHERE type='w' AND name=%Q AND rid!=%d", |
| 1136 | + zName, rid |
| 1137 | + ); |
| 1138 | + } |
| 1139 | + } |
| 1140 | +} |
| 1141 | + |
| 1142 | +/* |
| 1143 | +** If the doc-glob and doc-br settings are valid for document search |
| 1144 | +** and if the latest check-in on doc-br is in the unindexed set of |
| 1145 | +** check-ins, then update all 'd' entries in FTSDOCS that have |
| 1146 | +** changed. |
| 1147 | +*/ |
| 1148 | +static void search_update_doc_index(void){ |
| 1149 | + const char *zDocBr = db_get("doc-branch","trunk"); |
| 1150 | + int ckid = zDocBr ? symbolic_name_to_rid(zDocBr,"ci") : 0; |
| 1151 | + double rTime; |
| 1152 | + char *zBrUuid; |
| 1153 | + if( ckid==0 ) return; |
| 1154 | + if( !db_exists("SELECT 1 FROM ftsdocs WHERE type='c' AND rid=%d" |
| 1155 | + " AND NOT idxed", ckid) ) return; |
| 1156 | + |
| 1157 | + /* If we get this far, it means that changes to 'd' entries are |
| 1158 | + ** required. */ |
| 1159 | + rTime = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", ckid); |
| 1160 | + zBrUuid = db_text("","SELECT substr(uuid,1,20) FROM blob WHERE rid=%d",ckid); |
| 1161 | + db_multi_exec( |
| 1162 | + "CREATE TEMP TABLE current_docs(rid INTEGER PRIMARY KEY, name);" |
| 1163 | + "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;" |
| 1164 | + "INSERT OR IGNORE INTO current_docs(rid, name)" |
| 1165 | + " SELECT blob.rid, foci.filename FROM foci, blob" |
| 1166 | + " WHERE foci.checkinID=%d AND blob.uuid=foci.uuid" |
| 1167 | + " AND %z", |
| 1168 | + ckid, glob_expr("foci.filename", db_get("doc-glob","")) |
| 1169 | + ); |
| 1170 | + db_multi_exec( |
| 1171 | + "DELETE FROM ftsidx WHERE docid IN" |
| 1172 | + " (SELECT rowid FROM ftsdocs WHERE type='d'" |
| 1173 | + " AND rid NOT IN (SELECT rid FROM current_docs))" |
| 1174 | + ); |
| 1175 | + db_multi_exec( |
| 1176 | + "DELETE FROM ftsdocs WHERE type='d'" |
| 1177 | + " AND rid NOT IN (SELECT rid FROM current_docs)" |
| 1178 | + ); |
| 1179 | + db_multi_exec( |
| 1180 | + "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed,label,url,mtime)" |
| 1181 | + " SELECT 'd', rid, name, 0," |
| 1182 | + " printf('Document: %%s',name)," |
| 1183 | + " printf('/doc/%q/%%s',urlencode(name))," |
| 1184 | + " %.17g" |
| 1185 | + " FROM current_docs", |
| 1186 | + zBrUuid, rTime |
| 1187 | + ); |
| 1188 | + db_multi_exec( |
| 1189 | + "INSERT INTO ftsidx(docid,stext)" |
| 1190 | + " SELECT rowid, stext FROM ftscontent WHERE type='d' AND NOT idxed" |
| 1191 | + ); |
| 1192 | + db_multi_exec( |
| 1193 | + "UPDATE ftsdocs SET idxed=1 WHERE type='d' AND NOT idxed" |
| 1194 | + ); |
| 1195 | +} |
| 1196 | + |
| 1197 | +/* |
| 1198 | +** Deal with all of the unindexed 'c' terms in FTSDOCS |
| 1199 | +*/ |
| 1200 | +static void search_update_checkin_index(void){ |
| 1201 | + db_multi_exec( |
| 1202 | + "INSERT INTO ftsidx(docid,stext)" |
| 1203 | + " SELECT rowid, stext('c',rid,NULL) FROM ftsdocs" |
| 1204 | + " WHERE type='c' AND NOT idxed;" |
| 1205 | + ); |
| 1206 | + db_multi_exec( |
| 1207 | + "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)" |
| 1208 | + " SELECT ftsdocs.rowid, 1, 'c', ftsdocs.rid, NULL," |
| 1209 | + " printf('Check-in [%%.16s] on %%s',blob.uuid,datetime(event.mtime))," |
| 1210 | + " printf('/timeline?y=ci&n=9&c=%%.20s',blob.uuid)," |
| 1211 | + " event.mtime" |
| 1212 | + " FROM ftsdocs, event, blob" |
| 1213 | + " WHERE ftsdocs.type='c' AND NOT ftsdocs.idxed" |
| 1214 | + " AND event.objid=ftsdocs.rid" |
| 1215 | + " AND blob.rid=ftsdocs.rid" |
| 1216 | + ); |
| 1217 | +} |
| 1218 | + |
| 1219 | +/* |
| 1220 | +** Deal with all of the unindexed 't' terms in FTSDOCS |
| 1221 | +*/ |
| 1222 | +static void search_update_ticket_index(void){ |
| 1223 | + db_multi_exec( |
| 1224 | + "INSERT INTO ftsidx(docid,stext)" |
| 1225 | + " SELECT rowid, stext('t',rid,NULL) FROM ftsdocs" |
| 1226 | + " WHERE type='t' AND NOT idxed;" |
| 1227 | + ); |
| 1228 | + if( db_changes()==0 ) return; |
| 1229 | + db_multi_exec( |
| 1230 | + "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)" |
| 1231 | + " SELECT ftsdocs.rowid, 1, 't', ftsdocs.rid, NULL," |
| 1232 | + " printf('Ticket [%%.16s] on %%s',tkt_uuid,datetime(tkt_mtime))," |
| 1233 | + " printf('/tktview/%%.20s',tkt_uuid)," |
| 1234 | + " tkt_mtime" |
| 1235 | + " FROM ftsdocs, ticket" |
| 1236 | + " WHERE ftsdocs.type='t' AND NOT ftsdocs.idxed" |
| 1237 | + " AND ticket.tkt_id=ftsdocs.rid" |
| 1238 | + ); |
| 1239 | +} |
| 1240 | + |
| 1241 | +/* |
| 1242 | +** Deal with all of the unindexed 'w' terms in FTSDOCS |
| 1243 | +*/ |
| 1244 | +static void search_update_wiki_index(void){ |
| 1245 | + db_multi_exec( |
| 1246 | + "INSERT INTO ftsidx(docid,stext)" |
| 1247 | + " SELECT rowid, stext('w',rid,NULL) FROM ftsdocs" |
| 1248 | + " WHERE type='w' AND NOT idxed;" |
| 1249 | + ); |
| 1250 | + if( db_changes()==0 ) return; |
| 1251 | + db_multi_exec( |
| 1252 | + "REPLACE INTO ftsdocs(rowid,idxed,type,rid,name,label,url,mtime)" |
| 1253 | + " SELECT ftsdocs.rowid, 1, 'w', ftsdocs.rid, ftsdocs.name," |
| 1254 | + " 'Wiki: '||ftsdocs.name," |
| 1255 | + " '/wiki?name='||urlencode(ftsdocs.name)," |
| 1256 | + " tagxref.mtime" |
| 1257 | + " FROM ftsdocs, tagxref" |
| 1258 | + " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed" |
| 1259 | + " AND tagxref.rid=ftsdocs.rid" |
| 1260 | + ); |
| 1261 | +} |
| 1262 | + |
| 1263 | +/* |
| 1264 | +** Deal with all of the unindexed entries in the FTSDOCS table - that |
| 1265 | +** is to say, all the entries with FTSDOCS.IDXED=0. Add them to the |
| 1266 | +** index. |
| 1267 | +*/ |
| 1268 | +void search_update_index(unsigned int srchFlags){ |
| 1269 | + if( !search_index_exists() ) return; |
| 1270 | + if( !db_exists("SELECT 1 FROM ftsdocs WHERE NOT idxed") ) return; |
| 1271 | + search_sql_setup(g.db); |
| 1272 | + if( srchFlags & (SRCH_CKIN|SRCH_DOC) ){ |
| 1273 | + search_update_doc_index(); |
| 1274 | + search_update_checkin_index(); |
| 1275 | + } |
| 1276 | + if( srchFlags & SRCH_TKT ){ |
| 1277 | + search_update_ticket_index(); |
| 1278 | + } |
| 1279 | + if( srchFlags & SRCH_WIKI ){ |
| 1280 | + search_update_wiki_index(); |
| 1281 | + } |
| 1282 | +} |
| 1283 | + |
| 1284 | +/* |
| 1285 | +** COMMAND: test-fts |
| 1286 | +*/ |
| 1287 | +void test_fts_cmd(void){ |
| 1288 | + char *zSubCmd; |
| 1289 | + int i, n; |
| 1290 | + static const struct { int iCmd; const char *z; } aCmd[] = { |
| 1291 | + { 1, "create" }, |
| 1292 | + { 2, "drop" }, |
| 1293 | + { 3, "exists" }, |
| 1294 | + { 4, "fill" }, |
| 1295 | + { 8, "refill" }, |
| 1296 | + { 5, "pending" }, |
| 1297 | + { 7, "update" }, |
| 1298 | + }; |
| 1299 | + db_find_and_open_repository(0, 0); |
| 1300 | + if( g.argc<3 ) usage("SUBCMD ..."); |
| 1301 | + zSubCmd = g.argv[2]; |
| 1302 | + n = (int)strlen(zSubCmd); |
| 1303 | + for(i=0; i<ArraySize(aCmd); i++){ |
| 1304 | + if( fossil_strncmp(aCmd[i].z, zSubCmd, n)==0 ) break; |
| 1305 | + } |
| 1306 | + if( i>=ArraySize(aCmd) ){ |
| 1307 | + Blob all; |
| 1308 | + blob_init(&all,0,0); |
| 1309 | + for(i=0; i<ArraySize(aCmd); i++) blob_appendf(&all, " %s", aCmd[i].z); |
| 1310 | + fossil_fatal("unknown \"%s\" - should be:%s", zSubCmd, blob_str(&all)); |
| 1311 | + return; |
| 1312 | + } |
| 1313 | + db_begin_transaction(); |
| 1314 | + switch( aCmd[i].iCmd ){ |
| 1315 | + case 1: { assert( fossil_strncmp(zSubCmd, "create", n)==0 ); |
| 1316 | + search_create_index(); |
| 1317 | + break; |
| 1318 | + } |
| 1319 | + case 2: { assert( fossil_strncmp(zSubCmd, "drop", n)==0 ); |
| 1320 | + search_drop_index(); |
| 1321 | + break; |
| 1322 | + } |
| 1323 | + case 3: { assert( fossil_strncmp(zSubCmd, "exists", n)==0 ); |
| 1324 | + fossil_print("search_index_exists() = %d\n", search_index_exists()); |
| 1325 | + break; |
| 1326 | + } |
| 1327 | + case 4: { assert( fossil_strncmp(zSubCmd, "fill", n)==0 ); |
| 1328 | + search_fill_index(); |
| 1329 | + break; |
| 1330 | + } |
| 1331 | + case 8: { assert( fossil_strncmp(zSubCmd, "refill", n)==0 ); |
| 1332 | + search_drop_index(); |
| 1333 | + search_create_index(); |
| 1334 | + search_fill_index(); |
| 1335 | + break; |
| 1336 | + } |
| 1337 | + case 5: { assert( fossil_strncmp(zSubCmd, "pending", n)==0 ); |
| 1338 | + Stmt q; |
| 1339 | + if( !search_index_exists() ) break; |
| 1340 | + db_prepare(&q, "SELECT rowid,type,rid,quote(name) FROM ftsdocs" |
| 1341 | + " WHERE NOT idxed"); |
| 1342 | + while( db_step(&q)==SQLITE_ROW ){ |
| 1343 | + fossil_print("%6d: %s %6d %s\n", |
| 1344 | + db_column_int(&q, 0), |
| 1345 | + db_column_text(&q, 1), |
| 1346 | + db_column_int(&q, 2), |
| 1347 | + db_column_text(&q, 3) |
| 1348 | + ); |
| 1349 | + } |
| 1350 | + db_finalize(&q); |
| 1351 | + break; |
| 1352 | + } |
| 1353 | + case 7: { assert( fossil_strncmp(zSubCmd, "update", n)==0 ); |
| 1354 | + search_update_index(SRCH_ALL); |
| 1355 | + break; |
| 1356 | + } |
| 1357 | + |
| 1358 | + } |
| 1359 | + db_end_transaction(0); |
| 1360 | +} |
| 923 | 1361 | |