Browse Source

* HTML search database example

git-svn-id: trunk@39415 -
michael 7 years ago
parent
commit
ed4b94706b

+ 7 - 0
.gitattributes

@@ -3462,7 +3462,14 @@ packages/fpindexer/examples/TestIndexer.lpi svneol=native#text/plain
 packages/fpindexer/examples/TestIndexer.pp svneol=native#text/plain
 packages/fpindexer/examples/TestSearch.lpi svneol=native#text/plain
 packages/fpindexer/examples/TestSearch.pp svneol=native#text/plain
+packages/fpindexer/examples/docindexer.lpi svneol=native#text/plain
+packages/fpindexer/examples/docindexer.pp svneol=native#text/plain
 packages/fpindexer/examples/english.txt svneol=native#text/plain
+packages/fpindexer/examples/httpsearch.lpi svneol=native#text/plain
+packages/fpindexer/examples/httpsearch.pas svneol=native#text/plain
+packages/fpindexer/examples/httpsearcher.pp svneol=native#text/plain
+packages/fpindexer/examples/readme.txt svneol=native#text/plain
+packages/fpindexer/examples/sample.ini svneol=native#text/plain
 packages/fpindexer/fpmake.pp svneol=native#text/plain
 packages/fpindexer/src/dbindexer.pp svneol=native#text/plain
 packages/fpindexer/src/fbindexdb.pp svneol=native#text/plain

+ 104 - 0
packages/fpindexer/examples/docindexer.lpi

@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="11"/>
+    <PathDelim Value="\"/>
+    <General>
+      <Flags>
+        <SaveClosedFiles Value="False"/>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="docindexer"/>
+      <ResourceType Value="res"/>
+      <UseXPManifest Value="True"/>
+    </General>
+    <i18n>
+      <EnableI18N LFM="False"/>
+    </i18n>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <IncludeFileFilter Value="*.(pas|pp|inc|lfm|lpr|lrs|lpi|lpk|sh|xml)"/>
+      <ExcludeFileFilter Value="*.(bak|ppu|o|so);*~;backup"/>
+    </PublishOptions>
+    <RunParams>
+      <local>
+        <CommandLineParams Value="-e -d ~\dh\doc\fpdoc\ -d ~\dh\doc\prog\ -d ~\dh\doc\ref -d ~\dh\doc\user -p iso-8859-1 -c database.ini"/>
+      </local>
+      <FormatVersion Value="2"/>
+      <Modes Count="1">
+        <Mode0 Name="default">
+          <local>
+            <CommandLineParams Value="-e -d ~\dh\doc\fpdoc\ -d ~\dh\doc\prog\ -d ~\dh\doc\ref -d ~\dh\doc\user -p iso-8859-1 -c database.ini"/>
+          </local>
+        </Mode0>
+      </Modes>
+    </RunParams>
+    <Units Count="5">
+      <Unit0>
+        <Filename Value="docindexer.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="fpindexer.pp"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="fpIndexer"/>
+      </Unit1>
+      <Unit2>
+        <Filename Value="fpsearch.pp"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="fpSearch"/>
+      </Unit2>
+      <Unit3>
+        <Filename Value="sqliteindexdb.pp"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="SQLiteIndexDB"/>
+      </Unit3>
+      <Unit4>
+        <Filename Value="ireadertxt.pp"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="IReaderTXT"/>
+      </Unit4>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <PathDelim Value="\"/>
+    <Target>
+      <Filename Value="docindexer"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="..\src"/>
+      <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <CodeGeneration>
+      <Optimizations>
+        <OptimizationLevel Value="3"/>
+      </Optimizations>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <UseHeaptrc Value="True"/>
+      </Debugging>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 307 - 0
packages/fpindexer/examples/docindexer.pp

@@ -0,0 +1,307 @@
+program docindexer;
+
+{$mode objfpc}{$H+}
+{$IFDEF UNIX}
+  {$linklib pthread}
+{$ENDIF}
+
+uses
+  cwstring, cthreads, SysUtils, Classes, DateUtils, sqldb, SQLDBindexDB, FBindexDB, sqliteindexdb,  pgindexdb, memindexdb, fpIndexer, inifiles,
+  // indexer readers
+  IReaderTXT, IReaderPAS, IReaderHTML, CustApp;
+
+Type
+  { TDocIndexerApplication }
+
+  TDocIndexerApplication = class(TCustomApplication)
+  Private
+    FDirs : TStringArray;
+    FCreateDB : Boolean;
+    FEmptyDB : Boolean;
+    FStripPath,
+    FLanguage,
+    FIgnoreList,
+    FConfig : String;
+    FCommitFiles,
+    FLogSQL : Boolean;
+    FCodePage : TSystemCodePage;
+  Protected
+    Procedure WriteLog(Const Msg : String); virtual;
+    Procedure WriteLog(Const Fmt : String; Const Args : Array of Const);
+    procedure IndexLog(Sender : TObject; Const ACurrent,ACount : Integer; Const AURL : UTF8String);
+    Procedure DBHook(Sender : TSQLConnection; EventType : TDBEventType; Const Msg : String);
+    function ParseOptions: Boolean; virtual;
+    function SetupDB : TCustomIndexDB; virtual;
+    procedure CreateDB(aDB : TCustomIndexDB);virtual;
+    procedure ClearDB(aDB : TCustomIndexDB);virtual;
+    procedure DoIndex(aDB: TCustomIndexDB);virtual;
+    procedure Usage(const Msg: String);virtual;
+    Procedure DoRun; override;
+  Public
+    Constructor Create(aOwner : TComponent); override;
+  end;
+
+procedure TDocIndexerApplication.CreateDB(aDB : TCustomIndexDB);
+
+begin
+  WriteLog('Creating database');
+  aDB.CreateDB;
+end;
+
+procedure TDocIndexerApplication.ClearDB(aDB: TCustomIndexDB);
+begin
+  WriteLog('Clearing database tables');
+  aDB.CreateIndexerTables;
+end;
+
+function TDocIndexerApplication.SetupDB : TCustomIndexDB;
+
+Const
+  SDatabase = 'Database';
+  KeyHostName = 'HostName';
+  KeyDatabaseName = 'DatabaseName';
+  KeyUser = 'User';
+  KeyPassword = 'Password';
+  KeyType = 'Type';
+
+  Procedure ConfigSQLDB(DB : TSQLDBIndexDB; aIni : TInifile);
+
+  begin
+    DB.HostName:= aIni.ReadString(SDatabase,KeyHostName,DB.HostName);
+    DB.DatabasePath := aIni.ReadString(SDatabase,KeyDatabaseName,DB.DatabasePath);
+    DB.UserName := aIni.ReadString(SDatabase,KeyUser,DB.UserName);
+    DB.Password := aIni.ReadString(SDatabase,KeyPassword,DB.Password);
+  end;
+
+  Procedure ConfigSQLIte(SDB : TSQLiteIndexDB; aIni : TInifile);
+
+  begin
+    SDB.FileName := aIni.ReadString(SDatabase,KeyDatabaseName,SDB.FileName);
+  end;
+
+  Procedure ConfigFile(FDB : TFileIndexDB; aIni : TInifile);
+
+  begin
+    FDB.FileName := aIni.ReadString(SDatabase,KeyDatabaseName,FDB.FileName);
+  end;
+
+Var
+  Ini : TIniFile;
+  DB : TSQLDBIndexDB;
+  SDB : TSQLiteIndexDB;
+  FDB :  TFileIndexDB;
+
+begin
+  if FLogSQL then
+    GlobalDBLogHook:=@DBHook;
+  Result:=nil;
+  Ini:=TIniFile.Create(FConfig);
+  try
+    Case lowercase(Ini.ReadString(SDatabase,KeyType,'PostGres')) of
+      'postgres' :
+          begin
+          DB := TPGIndexDB.Create(nil);
+          ConfigSQLDB(DB,Ini);
+          Result:=DB;
+          end;
+      'firebird' :
+          begin
+          DB := TFBIndexDB.Create(nil);
+          ConfigSQLDB(DB,Ini);
+          Result:=DB;
+          end;
+      'sqlite' :
+          begin
+          SDB := TSQLiteIndexDB.Create(nil);
+          ConfigSQLite(SDB,Ini);
+          Result:=SDB;
+          end;
+      'file' :
+          begin
+          FDB := TFileIndexDB.Create(nil);
+          ConfigFile(FDB,Ini);
+          Result:=FDB;
+          end;
+    else
+      Raise Exception.CreateFmt('Unknown database type: "%s" ',[Ini.ReadString(SDatabase,KeyType,'PostGres')]);
+    end;
+  finally
+    ini.Free;
+  end;
+end;
+
+Procedure TDocIndexerApplication.DoIndex(aDB  : TCustomIndexDB);
+
+var
+  Indexer: TFPIndexer; //indexes files
+  start: TDateTime;
+  Dn,n: int64;
+  endtime: TDateTime;
+  D : String;
+
+begin
+  //SetHeapTraceOutput('heap.trc');
+  start := Now;
+  Indexer := TFPIndexer.Create(Nil);
+  try
+    Indexer.CodePage:=FCodePage;
+    Indexer.Database:=aDB;
+    //setup parameters for indexing
+    Indexer.FileMask := '*.pas;*.html;readme.txt'; //semicolon separated list
+    Indexer.SearchRecursive := True;
+    Indexer.DetectLanguage := False;
+    if (FIgnoreList<>'') then
+      IgnoreListManager.LoadIgnoreWordsFromFile(FLanguage,FIgnoreList);
+    indexer.Language:=FLanguage;
+    Indexer.UseIgnoreList:=true;
+    Indexer.CommitFiles:=FCommitFiles;
+    Indexer.StripPath:=FStripPath;
+    Indexer.OnProgress:=@IndexLog;
+    N:=0;
+    DN:=0;
+    For D in FDirs do
+      begin
+      inc(DN);
+      IndexLog(Self,-1,-1,Format('Treating directory %d of %d: %s',[DN,Length(FDirs),D]));
+      Indexer.SearchPath:=D;
+      //execute the search
+      N := N+Indexer.Execute(False);
+      end;
+    endtime := Now;
+    if N <> 0 then
+      WriteLog('Endexing succesful')
+    else
+      WriteLog('Error indexing or no words found...');
+    WriteLog(Format('Done, indexed %d words in %d directories in %d sec.', [N,Length(FDirs),SecondsBetween(endtime,start)]));
+  finally
+    FreeAndNil(Indexer);
+  end;
+end;
+
+Procedure TDocIndexerApplication.Usage(Const Msg : String);
+
+begin
+  If (Msg<>'') then
+    Writeln(Msg);
+  ExitCode:=Ord(Msg<>'')
+end;
+
+Function TDocIndexerApplication.ParseOptions : Boolean;
+
+Var
+  Enc : String;
+
+begin
+  Result:=True;
+  FConfig:=GetOptionValue('c','config');
+  If (FConfig='') then
+    begin
+    Usage('Need database connection configuration file');
+    Exit(False);
+    end;
+  FDirs:=GetOptionValues('d','directory');
+  if (Length(FDirs)=0) then
+    begin
+    SetLength(FDirs,1);
+    FDirs[0]:='.';
+    end;
+  FCreateDB:=HasOption('r','createdb');
+  FEmptyDB:=(Not FCreateDB) and HasOption('e','cleardb');
+  FLogSQL:=HasOption('q','querylog');
+  FCommitFiles:=HasOption('m','commit-files');
+  FLanguage:=GetOptionValue('l','language');
+  if FLanguage='' then
+    FLanguage:='english';
+  FIgnoreList:=GetOptionValue('i','ignore');
+  Enc:=getOptionValue('p','codepage');
+  FStripPath:=GetOptionValue('s','strip');
+  if Enc='' then
+    FCodePage:=CP_UTF8
+  else
+    begin
+    FCodePage := CodePageNameToCodePage(Enc);
+    if (FCodePage = $FFFF) then
+      begin
+      Usage('Invalid or unsupported encoding: '+Enc);
+      Exit(False);
+      end;
+    end;
+end;
+
+procedure TDocIndexerApplication.DoRun;
+
+Var
+  S : String;
+  DB : TCustomIndexDB;
+
+begin
+  Terminate;
+  S:=Checkoptions('hd:reqmc:l:i:p:s:',['help','directory','createdb','cleardb','querylog','commit-files','config','language','ignore-list','codepage','strip']);
+  if (S<>'') or HasOption('h','help') then
+    begin
+    Usage(S);
+    exit;
+    end;
+  If not ParseOptions then
+    exit;
+  DB:=SetupDB;
+  try
+    If FCreateDB then
+      DB.CreateDB
+    else
+      begin
+      DB.Connect;
+      if FEmptyDB then
+        ClearDB(DB);
+      end;
+    DoIndex(DB);
+  finally
+    DB.Free;
+  end;
+end;
+
+constructor TDocIndexerApplication.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  StopOnException:=True;
+  FCodePage:=CP_UTF8;
+end;
+
+procedure TDocIndexerApplication.WriteLog(const Msg: String);
+begin
+  Writeln(Msg);
+end;
+
+procedure TDocIndexerApplication.WriteLog(const Fmt: String; const Args: array of const);
+begin
+  WriteLog(Format(Fmt,Args));
+end;
+
+procedure TDocIndexerApplication.IndexLog(Sender: TObject; const ACurrent, ACount: Integer; const AURL: UTF8String);
+begin
+  if ACurrent=-1 then
+    WriteLog(AURL)
+  else
+    WriteLog('%5.2f%% [%d/%d] : %s',[(ACurrent/ACount*100),ACurrent,ACount,AURL]);
+end;
+
+procedure TDocIndexerApplication.DBHook(Sender: TSQLConnection; EventType: TDBEventType; const Msg: String);
+
+Var
+  S : String;
+
+begin
+  Str(EventType,S);
+  WriteLog('SQL [%s] : %s',[S,Msg]);
+end;
+
+begin
+  with TDocIndexerApplication.Create(Nil) do
+    try
+      Initialize;
+      Run;
+    finally
+      Free;
+    end;
+end.
+

+ 62 - 0
packages/fpindexer/examples/httpsearch.lpi

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="11"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="httpsearch"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+      <Modes Count="0"/>
+    </RunParams>
+    <Units Count="2">
+      <Unit0>
+        <Filename Value="httpsearch.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="httpsearcher.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="httpsearch"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../src"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 34 - 0
packages/fpindexer/examples/httpsearch.pas

@@ -0,0 +1,34 @@
+program httpsearch;
+
+// Undefine this to make a standalone HTTP server.
+// The standalone HTTP server listens on port 3010,
+// Change DefaultPort below to change this port.
+{$define usecgi}
+
+uses
+{$ifdef usecgi}
+  fpcgi,
+{$else}
+  fphttpapp,
+{$endif}
+  httpdefs, httproute, httpsearcher;
+
+{$ifndef usecgi}
+Const
+  DefaultPort = 3010;
+{$ENDIF}
+
+Var
+  aSearch : THTTPSearcher;
+
+begin
+  aSearch:=THTTPSearcher.Create(Application);
+  HTTPRouter.RegisterRoute('/search',@aSearch.HTMLSearch,true);
+  HTTPRouter.RegisterRoute('/list',@aSearch.WordList,False);
+  {$ifndef usecgi}
+  Application.Port:=DefaultPort;
+  {$endif}
+  Application.Initialize;
+  Application.Run;
+end.
+

+ 493 - 0
packages/fpindexer/examples/httpsearcher.pp

@@ -0,0 +1,493 @@
+unit httpsearcher;
+
+// You can remove the support you do not need.
+{$DEFINE USEFIREBIRD}
+{$DEFINE USESQLITE}
+{$DEFINE USEPOSTGRES}
+{$mode objfpc}{$H+}
+
+{$IFDEF USEFIREBIRD}
+{$DEFINE USESQLDB}
+{$ENDIF}
+{$IFDEF USEPOSTGRES}
+{$DEFINE USESQLDB}
+{$ENDIF}
+
+interface
+
+uses
+  Classes, SysUtils, DateUtils, sqldb,
+{$IFDEF USESQLDB}
+  SQLDBindexDB,
+{$ENDIF}
+{$IFDEF USEFIREBIRD}
+  FBindexDB, // Firebird support
+{$ENDIF}
+{$IFDEF USESQLITE}
+  sqliteindexdb, // sqlite 3 support
+{$ENDIF}
+{$IFDEF USEPOSTGRES}
+  pgindexdb, // Postgres support
+{$ENDIF}
+  memindexdb, // Custom Memory file. Always enabled
+  fpIndexer, inifiles, httpdefs, fpjson;
+
+Type
+  { THTTPSearcher }
+
+  THTTPSearcher = Class(TComponent)
+  private
+    FAllowCors: Boolean;
+    FDB : TCustomIndexDB;
+    FSearch : TFPSearch;
+    FDefaultMinRank : Integer;
+    FMinRank : Integer;
+    FFormattedJSON : Boolean;
+    FDefaultMetadata,
+    FIncludeMetaData : Boolean;
+    FDefaultAvailable : TAvailableMatch;
+    FMetadata : TJSONObject;
+    FWordsMetadata : TJSONObject;
+    procedure ConfigSearch(aRequest: TRequest; aResponse: TResponse);
+    procedure ConfigWordList(aRequest: TRequest; out aContaining : UTF8string; Out Partial : TAvailableMatch; Out aSimple : Boolean);
+    function SearchDataToJSON(aID: Integer; const aRes: TSearchWordData): TJSONObject;
+    procedure SendJSON(J: TJSONObject; aResponse: TResponse);
+    procedure SetupMetadata;
+  Protected
+    function InitSearch(aResponse: TResponse): Boolean;
+    function SetupDB(aIni: TCustomIniFile): TCustomIndexDB;
+    Property DB : TCustomIndexDB Read FDB;
+    Property Search : TFPSearch Read FSearch;
+    Property MinRank : Integer Read FMinRank;
+    Property FormattedJSON : Boolean Read FFormattedJSON;
+    Property AllowCors : Boolean Read FAllowCors;
+  Public
+    Function CheckParams(aRequest : TRequest; aResponse : TResponse) : Boolean;
+    Function CheckSearchParams(aRequest : TRequest; aResponse : TResponse) : Boolean;
+    Procedure HTMLSearch(aRequest : TRequest; aResponse : TResponse);
+    Procedure WordList(aRequest : TRequest; aResponse : TResponse);
+  end;
+
+
+implementation
+
+function THTTPSearcher.SetupDB(aini :TCustomIniFile) : TCustomIndexDB;
+
+Const
+  SDatabase = 'Database';
+  KeyType = 'Type';
+  KeyDatabaseName = 'DatabaseName';
+  {$IFDEF USESQLDB}
+  KeyHostName = 'HostName';
+  KeyUser = 'User';
+  KeyPassword = 'Password';
+  {$ENDIF}
+
+{$IFDEF USESQLDB}
+  Procedure ConfigSQLDB(DB : TSQLDBIndexDB);
+
+  begin
+    DB.HostName:= aIni.ReadString(SDatabase,KeyHostName,DB.HostName);
+    DB.DatabasePath := aIni.ReadString(SDatabase,KeyDatabaseName,DB.DatabasePath);
+    DB.UserName := aIni.ReadString(SDatabase,KeyUser,DB.UserName);
+    DB.Password := aIni.ReadString(SDatabase,KeyPassword,DB.Password);
+  end;
+{$ENDIF USESQLDB}
+
+{$IFDEF USESQLLITE}
+  Procedure ConfigSQLIte(SDB : TSQLiteIndexDB);
+
+  begin
+    SDB.FileName := aIni.ReadString(SDatabase,KeyDatabaseName,SDB.FileName);
+  end;
+{$ENDIF}
+
+  Procedure ConfigFile(FDB : TFileIndexDB);
+
+  begin
+    FDB.FileName := aIni.ReadString(SDatabase,KeyDatabaseName,FDB.FileName);
+  end;
+
+Var
+  {$IFDEF USESQLDB}
+  QDB : TSQLDBIndexDB;
+  {$ENDIF}
+  {$IFDEF USESQLLITE}
+  SDB : TSQLiteIndexDB;
+  {$ENDIF}
+  MDB :  TFileIndexDB;
+  aType : String;
+
+begin
+  Result:=nil;
+  aType:=aIni.ReadString(SDatabase,KeyType,'PostGres');
+  Case lowercase(aType) of
+{$IFDEF USEPOSTGRES}
+    'postgres' :
+        begin
+        QDB := TPGIndexDB.Create(nil);
+        ConfigSQLDB(QDB);
+        Result:=QDB;
+        end;
+{$ENDIF}
+{$IFDEF USEFIREBIRD}
+    'firebird' :
+        begin
+        QDB := TFBIndexDB.Create(nil);
+        ConfigSQLDB(QDB);
+        Result:=QDB;
+        end;
+{$ENDIF}
+{$IFDEF USESQLITE}
+    'sqlite' :
+        begin
+        SDB := TSQLiteIndexDB.Create(nil);
+        ConfigSQLite(SDB);
+        Result:=SDB;
+        end;
+{$ENDIF}
+    'file' :
+        begin
+        MDB := TFileIndexDB.Create(nil);
+        ConfigFile(MDB);
+        Result:=MDB;
+        end;
+  else
+    Raise Exception.CreateFmt('Unknown database type: "%s" ',[aType]);
+  end;
+end;
+
+function THTTPSearcher.CheckParams(aRequest: TRequest; aResponse: TResponse): Boolean;
+
+Var
+  S : String;
+  B : Boolean;
+
+begin
+  S:=aRequest.QueryFields.Values['q'];
+  Result:=S<>'';
+  if not Result then
+    begin
+    aResponse.Code:=400;
+    aResponse.CodeText:='Missing q param';
+    aResponse.SendResponse;
+    end;
+  S:=aRequest.QueryFields.Values['r'];
+  Result:=(S='') or (StrToIntDef(S,-1)<>-1);
+  if not Result then
+    begin
+    aResponse.Code:=400;
+    aResponse.CodeText:='Wrong value for r';
+    aResponse.SendResponse;
+    end;
+  S:=aRequest.QueryFields.Values['c'];
+  Result:=(S='') or TryStrToBool(S,B);
+  if not Result then
+    begin
+    aResponse.Code:=400;
+    aResponse.CodeText:='Wrong value for c';
+    aResponse.SendResponse;
+    end;
+  S:=aRequest.QueryFields.Values['m'];
+  Result:=(S='') or TryStrToBool(S,B);
+  if not Result then
+    begin
+    aResponse.Code:=400;
+    aResponse.CodeText:='Wrong value for m';
+    aResponse.SendResponse;
+    end;
+end;
+
+function THTTPSearcher.CheckSearchParams(aRequest: TRequest; aResponse: TResponse): Boolean;
+
+Var
+  m,S : String;
+  B : Boolean;
+
+begin
+  S:=aRequest.QueryFields.Values['q'];
+  M:=aRequest.QueryFields.Values['t'];
+  Result:=(M='');
+  if not Result then
+    case lowercase(M) of
+      'all' :
+        if S<>'' then
+          begin
+          aResponse.Code:=400;
+          aResponse.CodeText:='Q must be empty';
+          aResponse.SendResponse;
+          end;
+      'contains',
+      'exact',
+      'startswith' :
+        if S='' then
+          begin
+          aResponse.Code:=400;
+          aResponse.CodeText:='Q may not be empty';
+          aResponse.SendResponse;
+          end;
+    else
+      aResponse.Code:=400;
+      aResponse.CodeText:='Wrong value for t';
+      aResponse.SendResponse;
+    end;
+  S:=aRequest.QueryFields.Values['s'];
+  Result:=(S='') or TryStrToBool(S,B);
+  if not Result then
+    begin
+    aResponse.Code:=400;
+    aResponse.CodeText:='Wrong value for s';
+    aResponse.SendResponse;
+    end;
+  if not B then
+    begin
+    S:=aRequest.QueryFields.Values['m'];
+    Result:=(S='') or TryStrToBool(S,B);
+    if not Result then
+      begin
+      aResponse.Code:=400;
+      aResponse.CodeText:='Wrong value for m';
+      aResponse.SendResponse;
+      end;
+    end;
+end;
+
+Procedure THTTPSearcher.SetupMetadata;
+
+begin
+  FMetadata:=TJSONObject.Create([
+    'root', 'data',
+    'idField','id',
+    'fields',TJSONArray.Create([
+      TJSONObject.Create(['name','id','type','int']),
+      TJSONObject.Create(['name','rank','type','int']),
+      TJSONObject.Create(['name','url','type','string','maxlen',100]),
+      TJSONObject.Create(['name','context','type','string','maxlen',MaxContextLen]),
+      TJSONObject.Create(['name','date','type','date'])
+     ])
+  ]);
+  FWordsMetadata:=TJSONObject.Create([
+    'root', 'data',
+    'idField','id',
+    'fields',TJSONArray.Create([
+      TJSONObject.Create(['name','id','type','int']),
+      TJSONObject.Create(['name','word','type','string','maxlen',100])
+     ])
+  ]);
+end;
+
+Function THTTPSearcher.InitSearch(aResponse : TResponse): Boolean;
+
+Const
+  BaseName ='docsearch.ini';
+
+  Function TestCfg(aDir : string) : String;
+
+  begin
+    Result:=aDir+BaseName;
+    if not FileExists(Result) then
+      Result:='';
+  end;
+
+Var
+  CFN : String;
+  aIni: TMemIniFile;
+
+begin
+  Result:=False;
+  if FDB<>Nil then
+    exit(True);
+  try
+    CFN:=TestCfg(GetAppConfigDir(true));
+    if (CFN='') then
+      CFN:=TestCfg(GetAppConfigDir(False));
+    if (CFN='') then
+      CFN:=TestCfg('config/');
+    if (CFN='') then
+      CFN:=TestCfg(ExtractFilePath(ParamStr(0)));
+    if (CFN='') then
+      CFN:=TestCfg('');
+    if (CFN='') then
+      Raise Exception.Create('No config file found');
+    aIni:=TMemIniFile.Create(CFN);
+    try
+      FDB:=SetupDB(aIni);
+      FFormattedJSON:=aIni.ReadBool('search','formatjson',False);
+      FDefaultMinRank:=aIni.ReadInteger('search','minrank',1);
+      FDefaultMetadata:=aIni.ReadBool('search','metadata',true);
+      FAllowCors:=aIni.ReadBool('search','allowcors',true);
+    finally
+      aIni.Free;
+    end;
+    SetupMetadata;
+    FSearch:=TFPSearch.Create(Self);
+    FSearch.Database:=FDB;
+    Result:=True;
+  except
+    On E : Exception do
+      begin
+      aResponse.Code:=500;
+      aResponse.CodeText:='Could not set up search: '+E.Message;
+      aResponse.SendResponse;
+      end;
+  end;
+end;
+
+Procedure THTTPSearcher.ConfigSearch(aRequest : TRequest; aResponse : TResponse);
+
+Var
+  S : string;
+  O : TSearchOptions;
+  B : Boolean;
+
+begin
+  FMinRank:=StrToIntDef(aRequest.QueryFields.Values['r'],0);
+  if FMinRank=0 then
+    FMinRank:=FDefaultMinRank;
+  S:=aRequest.QueryFields.Values['m'];
+  if (S='') or Not TryStrToBool(S,FIncludeMetaData)  then
+    FIncludeMetaData:=FDefaultMetaData;
+  FSearch.SetSearchWord(aRequest.QueryFields.Values['q']);
+  O:=[];
+  S:=aRequest.QueryFields.Values['c'];
+  if (S<>'') and TryStrToBool(S,B) and B then
+    Include(O,soContains);
+  FSearch.Options:=O;
+end;
+
+procedure THTTPSearcher.ConfigWordList(aRequest: TRequest; out aContaining: UTF8string; out Partial: TAvailableMatch; out aSimple: Boolean);
+
+Var
+  m,S : String;
+
+begin
+  aContaining:=aRequest.QueryFields.Values['q'];
+  M:=aRequest.QueryFields.Values['t'];
+  case lowercase(M) of
+    'all' : Partial:=amAll;
+    'contains' : Partial:=amContains;
+    'exact' : Partial:=amExact;
+    'startswith' : Partial:=amStartsWith;
+  else
+    Partial:=FDefaultAvailable;
+    if (Partial<>amAll) and (aContaining='') then
+      Partial:=amAll;
+  end;
+  S:=aRequest.QueryFields.Values['s'];
+  if (S='') then
+    aSimple:=False
+  else
+    aSimple:=StrToBool(S);
+  if ASimple then
+    FIncludeMetadata:=False
+  else
+    begin
+    FIncludeMetaData:=FDefaultMetaData;
+    S:=aRequest.QueryFields.Values['m'];
+    if (S<>'') then
+      TryStrToBool(S,FIncludeMetaData);
+    end
+end;
+
+Function THTTPSearcher.SearchDataToJSON(aID : Integer;const aRes : TSearchWordData) : TJSONObject;
+
+begin
+  Result:=TJSONObject.Create([
+    'id',aID,
+    'rank',aRes.Rank,
+    'url',aRes.URL,
+    'context',ares.Context,
+    'date',FormatDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss',aRes.FileDate)
+  ]);
+end;
+
+procedure THTTPSearcher.HTMLSearch(aRequest: TRequest; aResponse: TResponse);
+
+Var
+  I : Integer;
+  J : TJSONObject;
+  A : TJSONArray;
+
+begin
+  aResponse.ContentType:='application/json';
+  if AllowCORS then
+    AResponse.SetCustomHeader('Access-Control-Allow-Origin','*');
+  if not CheckParams(aRequest,aResponse) then
+    exit;
+  if not InitSearch(aResponse) then
+    exit;
+  ConfigSearch(aRequest,aResponse);
+  FSearch.Execute;
+  A:=nil;
+  J:=TJSONObject.Create;
+  try
+    if FIncludeMetadata then
+      J.Add('metaData',FMetadata.Clone);
+    A:=TJSONArray.Create;
+    For I:=0 to Search.RankedCount-1 do
+      begin
+      if Search.RankedResults[I].Rank>=MinRank then
+        A.Add(SearchDataToJSON(I+1,Search.RankedResults[I]));
+      end;
+    J.Add('data',A);
+    SendJSON(J,aResponse);
+  finally
+    J.Free;
+  end;
+end;
+
+procedure THTTPSearcher.SendJSON(J : TJSONObject; aResponse: TResponse);
+
+begin
+  if FormattedJSON then
+    aResponse.Content:=J.FormatJSON()
+  else
+    aResponse.Content:=J.AsJSON;
+  aResponse.ContentLength:=Length(aResponse.Content);
+  aResponse.SendContent;
+end;
+
+procedure THTTPSearcher.WordList(aRequest: TRequest; aResponse: TResponse);
+Var
+  I : Integer;
+  J : TJSONObject;
+  A : TJSONArray;
+  w,aContaining : UTF8String;
+  aPartial : TAvailableMatch;
+  aSimple : Boolean;
+  aList : TUTF8StringArray;
+
+
+begin
+  aResponse.ContentType:='application/json';
+  if AllowCORS then
+    AResponse.SetCustomHeader('Access-Control-Allow-Origin','*');
+  if not CheckSearchParams(aRequest,aResponse) then
+    exit;
+  if not InitSearch(aResponse) then
+    exit;
+  ConfigWordList(aRequest,aContaining,aPartial,aSimple);
+  FSearch.GetAvailableWords(aList,aContaining,aPartial);
+  J:=TJSONObject.Create;
+  try
+    if FIncludeMetadata then
+      J.Add('metaData',FWordsMetadata.Clone);
+    A:=TJSONArray.Create;
+    if aSimple then
+      For W in aList do
+        A.Add(W)
+      else
+        begin
+        For I:=0 to Length(aList)-1 do
+          A.Add(TJSONObject.Create(['id',I+1,'word',aList[i]]));
+        end;
+    J.Add('data',A);
+    SendJSON(J,aResponse);
+  finally
+    J.Free;
+  end;
+end;
+
+
+end.
+

+ 61 - 0
packages/fpindexer/examples/readme.txt

@@ -0,0 +1,61 @@
+This directory contains some example how to create a searchable website.
+
+The docindexer program can be used to create the index.
+The httpsearch program is a HTTP server program that can search in the index.
+
+Both programs use an ini file that tells them what database must be used to
+store/consult the index.
+
+The docindexer essentially works as
+docindexer -c sample.ini -d /the/directory/to/index
+
+The httpsearch program can be compiled and started as a standalone HTTP server
+(no command-line args needed) 
+or can be compiled as a .cgi program. See the project source, the usecgi
+define can be enabled/disabled to switch between the behaviour.
+
+The programs support 4 database types:
+
+PostGres
+Firebird
+SQLite
+File
+
+Which ones are compiled-in depend on some defines in the httpsearcher.pp file
+ 
+The HTTP server supports 2 kinds of queries:
+[baseURL]/search  : search pages matching the search term.
+[baseURL]/list    : search words matching the search term. Can be used for typeahead funcionnality
+[baseURL] is the base URL where the HTTP service is listening...
+
+Responses are in JSON dataset format (see extjsdataset unit)
+
+The following HTTP Request query parameters are understood for "search":
+q - search term. Required.
+m - include metadata in response
+r - minimum rank for response (integer>0)
+c - Use "contains" to search. the default is  exact match (boolean: 0,1)
+
+The following HTTP Request query parameters are understood for "list":
+q - search term. Required unless type=all.
+m - include metadata in response
+t - Query type. One of
+    all
+    exact
+    contains
+    startswith
+    Determines how to search words. in case t=all, q must be empty.
+s - Return a simple array list.
+
+The sample.ini file can contain some default configuration settings.
+
+Defaults are shown
+[search]
+; Format the returned JSON (boolean)
+formatjson=0
+; Default for minimum rank in search results (integer)
+minrank=1 
+; Default for returning metadata (boolean)
+metadata=1
+; Set a CORS header on the response or not (boolean)
+allowcors=1

+ 25 - 0
packages/fpindexer/examples/sample.ini

@@ -0,0 +1,25 @@
+[Database]
+;Type: one of firebird,postgres,sqlite,file
+Type=File
+; For firebird, postgres: the hostname
+HostName=localhost
+; For all types: name of the daatbase
+;DatabaseName=indexer
+;DatabaseName=/home/firebird/index.fb
+DatabaseName=/tmp/index.dat
+;DatabaseName=/tmp/index.db
+
+; For postgres and firebird: username and password
+User=?
+Password=?
+
+
+[search]
+; Format the returned JSON
+formatjson=0
+; Default for minimum rank in search results
+minrank=1
+; Default for returning metadata
+metadata=1
+; Set a CORS header on the response or not
+allowcors=1