+ self.boost = 1.0
+
+ self._hits = []
+ self._processed_hits = None # processed hits
+
+ stored = search.searcher.doc(scoreDocs.doc)
+ self.book_id = int(stored.get("book_id"))
+
+ pd = stored.get("published_date")
+ try:
+ self.published_date = int(pd)
+ except ValueError:
+ self.published_date = 0
+
+ header_type = stored.get("header_type")
+ # we have a content hit in some header of fragment
+ if header_type is not None:
+ sec = (header_type, int(stored.get("header_index")))
+ header_span = stored.get('header_span')
+ header_span = header_span is not None and int(header_span) or 1
+
+ fragment = stored.get("fragment_anchor")
+
+ if snippets:
+ snippets = snippets.replace("/\n", "\n")
+ hit = (sec + (header_span,), fragment, scoreDocs.score, {'how_found': how_found, 'snippets': snippets and [snippets] or []})
+
+ self._hits.append(hit)
+
+ self.search = search
+ self.searched = searched
+ self.tokens_cache = tokens_cache
+
+ @property
+ def score(self):
+ return self._score * self.boost
+
+ def merge(self, other):
+ if self.book_id != other.book_id:
+ raise ValueError("this search result is or book %d; tried to merge with %d" % (self.book_id, other.book_id))
+ self._hits += other._hits
+ if other.score > self.score:
+ self._score = other._score
+ return self
+
+ def get_book(self):
+ if hasattr(self, '_book'):
+ return self._book
+ return catalogue.models.Book.objects.get(id=self.book_id)
+
+ book = property(get_book)
+
+ @property
+ def hits(self):
+ if self._processed_hits is not None:
+ return self._processed_hits
+
+ POSITION = 0
+ FRAGMENT = 1
+ POSITION_INDEX = 1
+ POSITION_SPAN = 2
+ SCORE = 2
+ OTHER = 3
+
+ # to sections and fragments
+ frags = filter(lambda r: r[FRAGMENT] is not None, self._hits)
+
+ sect = filter(lambda r: r[FRAGMENT] is None, self._hits)
+
+ # sections not covered by fragments
+ sect = filter(lambda s: 0 == len(filter(
+ lambda f: s[POSITION][POSITION_INDEX] >= f[POSITION][POSITION_INDEX]
+ and s[POSITION][POSITION_INDEX] < f[POSITION][POSITION_INDEX] + f[POSITION][POSITION_SPAN],
+ frags)), sect)
+
+ hits = []
+
+ def remove_duplicates(lst, keyfn, compare):
+ els = {}
+ for e in lst:
+ eif = keyfn(e)
+ if eif in els:
+ if compare(els[eif], e) >= 1:
+ continue
+ els[eif] = e
+ return els.values()
+
+ # remove fragments with duplicated fid's and duplicated snippets
+ frags = remove_duplicates(frags, lambda f: f[FRAGMENT], lambda a, b: cmp(a[SCORE], b[SCORE]))
+ frags = remove_duplicates(frags, lambda f: f[OTHER]['snippets'] and f[OTHER]['snippets'][0] or f[FRAGMENT],
+ lambda a, b: cmp(a[SCORE], b[SCORE]))
+
+ # remove duplicate sections
+ sections = {}
+
+ for s in sect:
+ si = s[POSITION][POSITION_INDEX]
+ # skip existing
+ if si in sections:
+ if sections[si]['score'] >= s[SCORE]:
+ continue
+
+ m = {'score': s[SCORE],
+ 'section_number': s[POSITION][POSITION_INDEX] + 1,
+ }
+ m.update(s[OTHER])
+ sections[si] = m
+
+ hits = sections.values()
+
+ for f in frags:
+ try:
+ frag = catalogue.models.Fragment.objects.get(anchor=f[FRAGMENT], book__id=self.book_id)
+ except catalogue.models.Fragment.DoesNotExist:
+ # stale index
+ continue
+
+ # Figure out if we were searching for a token matching some word in theme name.
+ themes = frag.tags.filter(category='theme')
+ themes_hit = []
+ if self.searched is not None:
+ tokens = self.search.get_tokens(self.searched, 'POLISH', cached=self.tokens_cache)
+ for theme in themes:
+ name_tokens = self.search.get_tokens(theme.name, 'POLISH')
+ for t in tokens:
+ if t in name_tokens:
+ if not theme in themes_hit:
+ themes_hit.append(theme)
+ break
+
+ m = {'score': f[SCORE],
+ 'fragment': frag,
+ 'section_number': f[POSITION][POSITION_INDEX] + 1,
+ 'themes': themes,
+ 'themes_hit': themes_hit
+ }
+ m.update(f[OTHER])
+ hits.append(m)
+
+ hits.sort(lambda a, b: cmp(a['score'], b['score']), reverse=True)
+
+ self._processed_hits = hits
+
+ return hits
+
+ def __unicode__(self):
+ return u'SearchResult(book_id=%d, score=%d)' % (self.book_id, self.score)
+
+ @staticmethod
+ def aggregate(*result_lists):
+ books = {}
+ for rl in result_lists:
+ for r in rl:
+ if r.book_id in books:
+ books[r.book_id].merge(r)
+ else:
+ books[r.book_id] = r
+ return books.values()
+
+ def __cmp__(self, other):
+ c = cmp(self.score, other.score)
+ if c == 0:
+ # this is inverted, because earlier date is better
+ return cmp(other.published_date, self.published_date)
+ else:
+ return c
+
+
+class Hint(object):
+ """
+ Given some hint information (information we already know about)
+ our search target - like author, title (specific book), epoch, genre, kind
+ we can narrow down search using filters.
+ """
+ def __init__(self, search):
+ """
+ Accepts a Searcher instance.
+ """
+ self.search = search
+ self.book_tags = {}
+ self.part_tags = []
+ self._books = []
+
+ def books(self, *books):
+ """
+ Give a hint that we search these books.
+ """
+ self._books = books
+
+ def tags(self, tags):
+ """
+ Give a hint that these Tag objects (a list of)
+ is necessary.
+ """
+ for t in tags:
+ if t.category in ['author', 'title', 'epoch', 'genre', 'kind']:
+ lst = self.book_tags.get(t.category, [])
+ lst.append(t)
+ self.book_tags[t.category] = lst
+ if t.category in ['theme', 'theme_pl']:
+ self.part_tags.append(t)
+
+ def tag_filter(self, tags, field='tags'):
+ """
+ Given a lsit of tags and an optional field (but they are normally in tags field)
+ returns a filter accepting only books with specific tags.
+ """
+ q = BooleanQuery()
+
+ for tag in tags:
+ toks = self.search.get_tokens(tag.name, field=field)
+ tag_phrase = PhraseQuery()
+ for tok in toks:
+ tag_phrase.add(Term(field, tok))
+ q.add(BooleanClause(tag_phrase, BooleanClause.Occur.MUST))
+
+ return QueryWrapperFilter(q)
+
+ def book_filter(self):
+ """
+ Filters using book tags (all tag kinds except a theme)
+ """
+ tags = reduce(lambda a, b: a + b, self.book_tags.values(), [])
+ if tags:
+ return self.tag_filter(tags)
+ else:
+ return None
+
+ def part_filter(self):
+ """
+ This filter can be used to look for book parts.
+ It filters on book id and/or themes.
+ """
+ fs = []
+ if self.part_tags:
+ fs.append(self.tag_filter(self.part_tags, field='themes'))
+
+ if self._books != []:
+ bf = BooleanFilter()
+ for b in self._books:
+ id_filter = NumericRangeFilter.newIntRange('book_id', b.id, b.id, True, True)
+ bf.add(FilterClause(id_filter, BooleanClause.Occur.SHOULD))
+ fs.append(bf)
+
+ return Search.chain_filters(fs)
+
+ def should_search_for_book(self):
+ return self._books == []
+
+ def just_search_in(self, all):
+ """Holds logic to figure out which indexes should be search, when we have some hinst already"""
+ some = []
+ for field in all:
+ if field == 'authors' and 'author' in self.book_tags:
+ continue
+ if field == 'title' and self._books != []:
+ continue
+ if (field == 'themes' or field == 'themes_pl') and self.part_tags:
+ continue
+ some.append(field)
+ return some
+
+
+class Search(IndexStore):
+ """
+ Search facilities.
+ """
+ def __init__(self, default_field="content"):
+ IndexStore.__init__(self)
+ self.analyzer = WLAnalyzer() # PolishAnalyzer(Version.LUCENE_34)
+ # self.analyzer = WLAnalyzer()
+ reader = IndexReader.open(self.store, True)
+ self.searcher = IndexSearcher(reader)
+ self.parser = QueryParser(Version.LUCENE_34, default_field,
+ self.analyzer)
+
+ self.parent_filter = TermsFilter()
+ self.parent_filter.addTerm(Term("is_book", "true"))
+ index_changed.connect(self.reopen)
+
+ def close(self):
+ reader = self.searcher.getIndexReader()
+ self.searcher.close()
+ reader.close()
+ super(Search, self).close()
+ index_changed.disconnect(self.reopen)
+
+ def reopen(self, **unused):
+ reader = self.searcher.getIndexReader()
+ rdr = reader.reopen()
+ if not rdr.equals(reader):
+ log.debug('Reopening index')
+ oldsearch = self.searcher
+ self.searcher = IndexSearcher(rdr)
+ oldsearch.close()
+ reader.close()
+
+ def query(self, query):
+ """Parse query in default Lucene Syntax. (for humans)
+ """
+ return self.parser.parse(query)
+
+ def simple_search(self, query, max_results=50):
+ """Runs a query for books using lucene syntax. (for humans)
+ Returns (books, total_hits)
+ """
+
+ tops = self.searcher.search(self.query(query), max_results)