194 lines
5.5 KiB
OCaml
194 lines
5.5 KiB
OCaml
(* Yoann Padioleau
|
|
*
|
|
* Copyright (C) 2010 Facebook
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public License
|
|
* version 2.1 as published by the Free Software Foundation, with the
|
|
* special exception on linking described in file license.txt.
|
|
*
|
|
* This library is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the file
|
|
* license.txt for more details.
|
|
*)
|
|
open Common
|
|
|
|
module Db = Database_code
|
|
|
|
(*****************************************************************************)
|
|
(* Prelude *)
|
|
(*****************************************************************************)
|
|
(*
|
|
* Inspired by 'tbgs' and big_grep at facebook.
|
|
* The trick is to build a giant string and run compiled-regexps
|
|
* on it. For each match have to go back to find the start and
|
|
* end of entity, or the entity number so can display
|
|
* the information associated with it. So need markers
|
|
* in the string.
|
|
*
|
|
* One-liner in perl by Erling:
|
|
* perl -e '$|++; open F,"/usr/share/dict/words"; { local $/; $all=<F>;
|
|
* } while(<STDIN>) { chomp; $w=$_; $n = 0; while($all =~ /$w.*/g) {
|
|
* print "$&\n"; last if ++$n>10; } print "[$w]\n"; }'
|
|
*
|
|
*)
|
|
|
|
(*****************************************************************************)
|
|
(* Types *)
|
|
(*****************************************************************************)
|
|
|
|
type index = {
|
|
big_string: string;
|
|
pos_to_entity: (int, Db.entity) Hashtbl.t;
|
|
case_sensitive: bool;
|
|
}
|
|
|
|
(* using \n is convenient so can allow regexp queries like
|
|
* employee.* without having the regexp engine to try to match
|
|
* the whole string; it will stop at the first \n.
|
|
*)
|
|
let separation_marker_char = '\n'
|
|
|
|
let empty_index () = {
|
|
big_string = "";
|
|
pos_to_entity = Hashtbl.create 1;
|
|
case_sensitive = false;
|
|
}
|
|
|
|
(*****************************************************************************)
|
|
(* Helpers *)
|
|
(*****************************************************************************)
|
|
|
|
let (==~) = Common2.(==~)
|
|
|
|
(*****************************************************************************)
|
|
(* Naive version *)
|
|
(*****************************************************************************)
|
|
|
|
(* This is the naive version, just to have a baseline for benchmarks *)
|
|
let naive_top_n_search2 ~top_n ~query xs =
|
|
let re = Str.regexp (".*" ^ query) in
|
|
|
|
let rec aux ~n xs =
|
|
if n = top_n
|
|
then []
|
|
else
|
|
(match xs with
|
|
| [] -> []
|
|
| e::xs ->
|
|
if e.Db.e_name ==~ re
|
|
then
|
|
e::aux ~n:(n+1) xs
|
|
else
|
|
aux ~n xs
|
|
)
|
|
in
|
|
aux ~n:0 xs
|
|
|
|
|
|
let naive_top_n_search ~top_n ~query idx =
|
|
Common.profile_code "Big_grep.naive_top_n" (fun () ->
|
|
naive_top_n_search2 ~top_n ~query idx
|
|
)
|
|
|
|
(*****************************************************************************)
|
|
(* Main entry point *)
|
|
(*****************************************************************************)
|
|
|
|
let build_index2 ?(case_sensitive=false) entities =
|
|
|
|
let buf = Buffer.create 20_000_000 in
|
|
let h = Hashtbl.create 1001 in
|
|
|
|
let current = ref 0 in
|
|
|
|
entities +> List.iter (fun e ->
|
|
(* Use fullname ? The caller, that is for instance
|
|
* files_and_dirs_and_sorted_entities_for_completion
|
|
* should have done the job of putting the fullename in e_name.
|
|
*)
|
|
let s = Common2.string_of_char separation_marker_char ^ e.Db.e_name in
|
|
let s =
|
|
if case_sensitive
|
|
then s
|
|
else Common2.lowercase s
|
|
in
|
|
|
|
Buffer.add_string buf s;
|
|
Hashtbl.add h !current e;
|
|
current := !current + String.length s;
|
|
);
|
|
(* just to make it easier to code certain algorithms such as
|
|
* find_position_marker_after
|
|
*)
|
|
Buffer.add_string buf (Common2.string_of_char separation_marker_char);
|
|
|
|
{
|
|
big_string = Buffer.contents buf;
|
|
pos_to_entity = h;
|
|
case_sensitive = case_sensitive;
|
|
}
|
|
|
|
let build_index ?case_sensitive a =
|
|
Common.profile_code "Big_grep.build_idx" (fun () ->
|
|
build_index2 ?case_sensitive a)
|
|
|
|
|
|
let find_position_marker_before start_pos str =
|
|
let pos = ref (start_pos - 1) in
|
|
|
|
while String.get str !pos <> separation_marker_char do
|
|
pos := !pos - 1
|
|
done;
|
|
!pos
|
|
|
|
let find_position_marker_after start_pos str =
|
|
let pos = ref (start_pos + 1) in
|
|
|
|
while String.get str !pos <> separation_marker_char do
|
|
pos := !pos + 1
|
|
done;
|
|
!pos
|
|
|
|
(* the query can now contain multipe words *)
|
|
let top_n_search2 ~top_n ~query idx =
|
|
|
|
let query =
|
|
if idx.case_sensitive then query else Common2.lowercase query
|
|
in
|
|
|
|
let words = Str.split (Str.regexp "[ \t]+") query in
|
|
let re =
|
|
match words with
|
|
| [_] -> Str.regexp (".*" ^ query)
|
|
| [a;b] ->
|
|
Str.regexp (spf
|
|
".*\\(%s.*%s\\)\\|\\(%s.*%s\\)"
|
|
a b b a)
|
|
| _ ->
|
|
failwith "more-than-2-words query is not supported; give money to pad"
|
|
in
|
|
|
|
let rec aux ~n ~pos =
|
|
if n = top_n
|
|
then []
|
|
else
|
|
try
|
|
let new_pos = Str.search_forward re idx.big_string pos in
|
|
(* let's found the marker *)
|
|
let pos_mark =
|
|
find_position_marker_before new_pos idx.big_string in
|
|
let pos_next_mark =
|
|
find_position_marker_after new_pos idx.big_string in
|
|
let e = Hashtbl.find idx.pos_to_entity pos_mark in
|
|
e::aux ~n:(n+1) ~pos:pos_next_mark
|
|
with Not_found -> []
|
|
in
|
|
aux ~n:0 ~pos:0
|
|
|
|
|
|
let top_n_search ~top_n ~query idx =
|
|
Common.profile_code "Big_grep.top_n" (fun () ->
|
|
top_n_search2 ~top_n ~query idx
|
|
)
|