JBR - 20101022
fts.tcl - index a directory tree of random docs.
I have been using swish-e and swish++ for a while but they are too slow and don't have very nice incremental index features. Here is a full text search system written in tcl using sqlite3's fts3 virtual tables. I'm a rank novice at sql and implemented the searchrank function as simply as possible so any suggestions are welcome. This entire program was written in 1 day, inspiration to wiki in 6 hours, enjoy.
The search system in configured with a configuration file similar to swish++. There are several high level commands, here are some example command lines:
./fts index myindex.conf ./fts search myindex.conf "search terms"
A little debugging output has been included and is selectable with a final arg to "fts index". The command gives a short summary if run with no args.
john@tdc : ./fts
fts: unknown subcommand ""
fts index <index.db> <conf> <verb> - index a set of directories indicated in <conf>
verbosity is a comma separated list of the message types insert, update, unindexed
, unchanged,exclude or a unique prefix of them.
fts search <index.db> <query> - seach the index for query
fts excludes <index.db> <conf> - display the exclude patterns from <conf>
fts filters <index.db> <conf> - display the filter patterns from <conf>
fts docs - display a table of documents in the index.
fts.conf is the default config file.
fts.conf:
set tmp /var/tmp stopwords stopwords filter *.bz2 bunzip2 -c %f > @%F filter *.gz gunzip -c %f > @%F filter *.Z uncompress -c %f > @%F filter *.doc catdoc %f filter *.xls xls2csv %f filter *.ppt catppt %f filter *.prn pdftotext %f - filter *.pdf pdftotext %f - filter *.PS ps2ascii %f filter *.ps ps2ascii %f filter *.eps ps2ascii %f filter *.txt cat %f filter *.htm lynx -dump %f filter *.html lynx -dump %f filter */page/* cat %f exclude *.dxf exclude *.exe exclude *.gif *.jpg *.png .jpeg exclude *.mpeg *.mov .avi exclude *.mf1 *.mf2 exclude *.sym exclude *.zip exclude *~ exclude *.files/ index-path /data/cgijohn/paige/file index-path /data/cgijohn/paige/page index-path /data/wdocs/john/idoc-db index-path /data/wdocs/dfabricant/www-docs
fts.tcl:
#!/home/john/bin/tclkit8.6
#
load /home/john/lib/libtclsqlite3.so
set verb QUIET
proc verb { type message } { if { [regexp $::verb $type] } { puts "[format %10.10s $type] : $message" } }
proc database { database } { set ::database $database }
set excludes {}
proc exclude { args } {
foreach pattern $args {
lappend ::excludes $pattern
if { [regexp {^\*\.[a-z0-9]+$} $pattern] } {
lappend ::excludes [string toupper $pattern]
}
}
}
proc exclude? { file } {
foreach pattern $::excludes {
if { [string match $pattern $file] } { return 1 }
}
return 0
}
proc filter { pattern args } {
lappend ::filters $pattern $args
if { [regexp {^\*\.[a-z0-9]+$} $pattern] } {
lappend ::filters [string toupper $pattern] $args
}
}
proc filter? { file } {
foreach { pattern action } $::filters {
if { [string match $pattern $file] } {
if { [string first @%F $action] == -1 } {
return [list [list [string map [list %f $file] $action]]]
} else {
set f $file
set F [string map [list [string range $pattern 1 end] {}] $::tmp/[file tail $file]]
set actions [list [string map [list %f $f @%F $F] $action] $F]
if { [set action [filter? $F]] eq {} } {
return {}
} else {
return [list $actions {*}$action]
}
}
}
}
return {}
}
proc filter! { actions } {
set tmpfiles {}
foreach action [lrange $actions 0 end-1] {
foreach { action tmpfile } $action {}
try { exec -ignorestderr {*}$action
} on error message {
puts $message
}
lappend tmpfiles $tmpfile
}
set indx {}
set body {}
try {
set body [exec -ignorestderr {*}[lindex [lindex $actions end] 0]]
} on error message {
puts $message
} finally {
if { $tmpfiles ne {} } { exec rm -f $tmpfiles }
}
return $body
}
proc stopwords { file } {
foreach word [split [read [set fp [open $file]]]] {
set ::stops($word) 1
}
close $fp
}
proc index-file { file } {
if { ![llength [set actions [filter? $file]]] } {
verb unindexed $file
return
}
try {
set xtime [file mtime $file]
} on error message {
puts $message
return
}
db eval { select mtime from documents where file = $file } {
if { $mtime >= $xtime } {
verb unchanged $file
return
}
}
set body [filter! $actions]
set body [regsub -all {\m[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][-+]?[0-9]+)?\M} $body { }]
set body [regsub -all {[,<>?\\~!@#$%^&*()_+]} $body { }]
foreach word [split $body] {
if { [regexp {[[:lower:]]} $word] } {
if { [string length $word] <= 3 } { continue }
set word [string tolower $word]
}
if { [info exists ::stops($word)] } { continue }
append indx $word " "
}
if { [db eval { select rowid from documents where file = @file }] eq {} } {
verb insert $file
db eval {
begin transaction ;
insert into documents ( file, mtime ) values ( $file, $xtime ) ;
insert into searchtext ( docid, body ) values ( last_insert_rowid(), $indx ) ;
commit transaction
}
} else {
verb update $file
db eval {
begin transaction ;
update documents set mtime = $xtime where file = $file ;
update searchtext set body = @body
where docid = ( select rowid from documents where file = $file ) ;
commit transaction
}
}
}
proc index-path { path } { lappend ::paths $path }
proc index-path! { path } {
foreach dir [glob -type d -directory $path -nocomplain *] {
if { [exclude? $dir/] } { continue }
index-path! $dir
}
foreach file [glob -type f -directory $path -nocomplain *] {
if { [exclude? $file] } { continue }
index-file $file
}
}
set command [lindex $argv 0]
set config [lindex $argv 1]
set query [lrange $argv 2 end]
if { $config eq {} } {
puts "fts: no config file"
exit 1
} else {
try { source $::config } on error message { puts "fts: $message"; exit 1 }
}
proc searchrank { matchinfo } {
binary scan $matchinfo iiiii nphrase ncol 1 2 3
return $1
}
switch $command {
docs -
index -
search {
sqlite3 db $database
db enable_load_extension 1
db function searchrank searchrank
catch { db eval { create virtual table searchtext using fts3(tokenize=porter, body text); } }
catch { db eval { create table documents ( file, mtime ) } }
catch { db eval { create index on documents ( file ) } }
}
default {
puts "fts: unknown subcommand \"$command\""
puts {
fts index <conf> <verb> - index a set of directories indicated in <conf>
verbosity is a comma separated list of the message types insert, update, unindexed
, unchanged,exclude or a unique prefix of them.
fts search <conf> <query> - seach the index for query
fts excludes <conf> - display the exclude patterns from <conf>
fts filters <conf> - display the filter patterns from <conf>
fts docs - display a table of documents in the index.
fts.conf is the default config file.
}
exit 1
}
}
switch $command {
excludes { foreach exclude $::excludes { puts $exclude } }
filters { foreach { pattern action } $::filters { puts "[format %8.8s $pattern] : $action" } }
index {
if { [llength $argv] == 3 } { set ::verb ^(([string map { , .*)|( } [lindex $argv 2]].*))$ }
set ::itime [file mtime $database]
foreach path $::paths { index-path! $path }
}
search {
db eval { select docid, searchrank(matchinfo(searchtext)) as rank from searchtext
where body match @query
order by searchrank(matchinfo(searchtext)) desc; } {
db eval { select rowid, file from documents where rowid = $docid ; } {
puts "$rank $file"
}
}
}
docs {
puts "rowid mtime file"
puts "----- ----- ----"
db eval { select rowid, file, mtime from documents } {
puts "$rowid $mtime $file"
}
}
}