X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/73ce961f14509aabfa26536f847afd28111029c6..faef1a564460905fd4c78953bf2f7023b5a94fc7:/apps/search/index.py diff --git a/apps/search/index.py b/apps/search/index.py index cc478ee53..6d97047a0 100644 --- a/apps/search/index.py +++ b/apps/search/index.py @@ -25,6 +25,7 @@ import re import errno from librarian import dcparser from librarian.parser import WLDocument +from lxml import etree import catalogue.models from multiprocessing.pool import ThreadPool from threading import current_thread @@ -212,7 +213,7 @@ class Index(BaseIndex): for tag in catalogue.models.Tag.objects.all(): doc = Document() - doc.add(NumericField("tag_id", Field.Store.YES, True).setIntValue(tag.id)) + doc.add(NumericField("tag_id", Field.Store.YES, True).setIntValue(int(tag.id))) doc.add(Field("tag_name", tag.name, Field.Store.NO, Field.Index.ANALYZED)) doc.add(Field("tag_name_pl", tag.name, Field.Store.NO, Field.Index.ANALYZED)) doc.add(Field("tag_category", tag.category, Field.Store.NO, Field.Index.NOT_ANALYZED)) @@ -223,9 +224,9 @@ class Index(BaseIndex): Create a lucene document referring book id. """ doc = Document() - doc.add(NumericField("book_id", Field.Store.YES, True).setIntValue(book.id)) + doc.add(NumericField("book_id", Field.Store.YES, True).setIntValue(int(book.id))) if book.parent is not None: - doc.add(NumericField("parent_id", Field.Store.YES, True).setIntValue(book.parent.id)) + doc.add(NumericField("parent_id", Field.Store.YES, True).setIntValue(int(book.parent.id))) return doc def remove_book(self, book): @@ -255,7 +256,7 @@ class Index(BaseIndex): self.index.addDocument(book_doc) del book_doc - self.index_content(book, book_fields=[meta_fields['title'], meta_fields['authors']]) + self.index_content(book, book_fields=[meta_fields['title'], meta_fields['authors'], meta_fields['published_date']]) master_tags = [ 'opowiadanie', @@ -263,10 +264,21 @@ class Index(BaseIndex): 'dramat_wierszowany_l', 'dramat_wierszowany_lp', 'dramat_wspolczesny', 'liryka_l', 'liryka_lp', - 'wywiad' + 'wywiad', ] - skip_header_tags = ['autor_utworu', 'nazwa_utworu', 'dzielo_nadrzedne'] + ignore_content_tags = [ + 'uwaga', 'extra', + 'zastepnik_tekstu', 'sekcja_asterysk', 'separator_linia', 'zastepnik_wersu', + 'didaskalia', + 'naglowek_aktu', 'naglowek_sceny', 'naglowek_czesc', + ] + + footnote_tags = ['pa', 'pt', 'pr', 'pe'] + + skip_header_tags = ['autor_utworu', 'nazwa_utworu', 'dzielo_nadrzedne', '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF'] + + published_date_re = re.compile("([0-9]+)[\]. ]*$") def extract_metadata(self, book, book_info=None): """ @@ -308,6 +320,12 @@ class Index(BaseIndex): fields[field.name] = Field(field.name, "%04d%02d%02d" %\ (dt.year, dt.month, dt.day), Field.Store.NO, Field.Index.NOT_ANALYZED) + # get published date + source = book_info.source_name + match = self.published_date_re.search(source) + if match is not None: + fields["published_date"] = Field("published_date", str(match.groups()[0]), Field.Store.YES, Field.Index.NOT_ANALYZED) + return fields def add_gaps(self, fields, fieldname): @@ -340,15 +358,26 @@ class Index(BaseIndex): if master is None: return [] - def walker(node): + def walker(node, ignore_tags=[]): yield node, None - for child in list(node): + for child in filter(lambda n: n.tag not in ignore_tags, list(node)): for b, e in walker(child): yield b, e yield None, node return def fix_format(text): + # separator = [u" ", u"\t", u".", u";", u","] + if isinstance(text, list): + # need to join it first + text = filter(lambda s: s is not None, content) + text = u' '.join(text) + # for i in range(len(text)): + # if i > 0: + # if text[i][0] not in separator\ + # and text[i - 1][-1] not in separator: + # text.insert(i, u" ") + return re.sub("(?m)/$", "", text) def add_part(snippets, **fields): @@ -401,53 +430,71 @@ class Index(BaseIndex): if header.tag in self.skip_header_tags: continue + if header.tag is etree.Comment: + continue - content = u' '.join([t for t in header.itertext()]) - content = fix_format(content) + # section content + content = [] + footnote = None - doc = add_part(snippets, header_index=position, header_type=header.tag, content=content) + for start, end in walker(header, ignore_tags=self.ignore_content_tags): + # handle footnotes + # if start is not None and start.tag in self.footnote_tags: + # footnote = ' '.join(start.itertext()) + # elif end is not None and footnote is not None and end.tag in self.footnote_tags: + # doc = add_part(snippets, header_index=position, header_type=header.tag, + # content=footnote) - self.index.addDocument(doc) + # self.index.addDocument(doc) + + # footnote = None - for start, end in walker(header): + # handle fragments and themes. if start is not None and start.tag == 'begin': fid = start.attrib['id'][1:] fragments[fid] = {'content': [], 'themes': [], 'start_section': position, 'start_header': header.tag} - fragments[fid]['content'].append(start.tail) + elif start is not None and start.tag == 'motyw': fid = start.attrib['id'][1:] if start.text is not None: fragments[fid]['themes'] += map(str.strip, map(give_me_utf8, start.text.split(','))) - fragments[fid]['content'].append(start.tail) + elif start is not None and start.tag == 'end': fid = start.attrib['id'][1:] if fid not in fragments: continue # a broken node, skip it + # import pdb; pdb.set_trace() frag = fragments[fid] if frag['themes'] == []: continue # empty themes list. del fragments[fid] - def jstr(l): - return u' '.join(map( - lambda x: x == None and u'(none)' or unicode(x), - l)) - doc = add_part(snippets, header_type=frag['start_header'], header_index=frag['start_section'], header_span=position - frag['start_section'] + 1, fragment_anchor=fid, - content=u' '.join(filter(lambda s: s is not None, frag['content'])), + content=fix_format(frag['content']), themes=frag['themes']) self.index.addDocument(doc) + + # Collect content. elif start is not None: for frag in fragments.values(): frag['content'].append(start.text) + content.append(start.text) elif end is not None: for frag in fragments.values(): frag['content'].append(end.tail) + content.append(end.tail) + + # in the end, add a section text. + doc = add_part(snippets, header_index=position, header_type=header.tag, + content=fix_format(content)) + + self.index.addDocument(doc) + finally: snippets.close() @@ -541,16 +588,20 @@ class JoinSearch(object): class SearchResult(object): - def __init__(self, searcher, scoreDocs, score=None, how_found=None, snippets=None): + def __init__(self, search, scoreDocs, score=None, how_found=None, snippets=None, searched=None, tokens_cache=None): + if tokens_cache is None: tokens_cache = {} + if score: - self.score = score + self._score = score else: - self.score = scoreDocs.score + self._score = scoreDocs.score + + self.boost = 1.0 self._hits = [] - self.hits = None # processed hits + self._processed_hits = None # processed hits - stored = searcher.doc(scoreDocs.doc) + stored = search.searcher.doc(scoreDocs.doc) self.book_id = int(stored.get("book_id")) header_type = stored.get("header_type") @@ -563,16 +614,31 @@ class SearchResult(object): fragment = stored.get("fragment_anchor") + pd = stored.get("published_date") + if pd is None: + pd = 0 + self.published_date = int(pd) + + 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 + self._score = other._score return self def get_book(self): @@ -580,7 +646,11 @@ class SearchResult(object): book = property(get_book) - def process_hits(self): + @property + def hits(self): + if self._processed_hits is not None: + return self._processed_hits + POSITION = 0 FRAGMENT = 1 POSITION_INDEX = 1 @@ -627,19 +697,39 @@ class SearchResult(object): hits = sections.values() for f in frags: - frag = catalogue.models.Fragment.objects.get(anchor=f[FRAGMENT]) + try: + frag = catalogue.models.Fragment.objects.get(anchor=f[FRAGMENT]) + 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, - 'themes': frag.tags.filter(category='theme') + '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.hits = hits + self._processed_hits = hits - return self + return hits def __unicode__(self): return u'SearchResult(book_id=%d, score=%d)' % (self.book_id, self.score) @@ -657,7 +747,12 @@ class SearchResult(object): return books.values() def __cmp__(self, other): - return cmp(self.score, other.score) + 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): @@ -787,11 +882,14 @@ class Search(IndexStore): bks.append(catalogue.models.Book.objects.get(id=doc.get("book_id"))) return (bks, tops.totalHits) - def get_tokens(self, searched, field='content'): + def get_tokens(self, searched, field='content', cached=None): """returns tokens analyzed by a proper (for a field) analyzer argument can be: StringReader, string/unicode, or tokens. In the last case they will just be returned (so we can reuse tokens, if we don't change the analyzer) """ + if cached is not None and field in cached: + return cached[field] + if isinstance(searched, str) or isinstance(searched, unicode): searched = StringReader(searched) elif isinstance(searched, list): @@ -803,6 +901,10 @@ class Search(IndexStore): while tokens.incrementToken(): cta = tokens.getAttribute(CharTermAttribute.class_) toks.append(cta.toString()) + + if cached is not None: + cached[field] = toks + return toks def fuzziness(self, fuzzy): @@ -859,9 +961,40 @@ class Search(IndexStore): q.add(BooleanClause(term, modal)) return q - # def content_query(self, query): - # return BlockJoinQuery(query, self.parent_filter, - # BlockJoinQuery.ScoreMode.Total) + def search_phrase(self, searched, field, book=True, max_results=20, fuzzy=False, + filters=None, tokens_cache=None, boost=None, snippets=False, slop=2): + if filters is None: filters = [] + if tokens_cache is None: tokens_cache = {} + + tokens = self.get_tokens(searched, field, cached=tokens_cache) + + query = self.make_phrase(tokens, field=field, fuzzy=fuzzy, slop=slop) + if book: + filters.append(self.term_filter(Term('is_book', 'true'))) + top = self.searcher.search(query, self.chain_filters(filters), max_results) + + return [SearchResult(self, found, snippets=(snippets and self.get_snippets(found, query) or None), searched=searched) for found in top.scoreDocs] + + def search_some(self, searched, fields, book=True, max_results=20, fuzzy=False, + filters=None, tokens_cache=None, boost=None, snippets=True): + if filters is None: filters = [] + if tokens_cache is None: tokens_cache = {} + + if book: + filters.append(self.term_filter(Term('is_book', 'true'))) + + query = BooleanQuery() + + for fld in fields: + tokens = self.get_tokens(searched, fld, cached=tokens_cache) + + query.add(BooleanClause(self.make_term_query(tokens, field=fld, + fuzzy=fuzzy), BooleanClause.Occur.SHOULD)) + + top = self.searcher.search(query, self.chain_filters(filters), max_results) + + return [SearchResult(self, found, searched=searched, tokens_cache=tokens_cache, + snippets=(snippets and self.get_snippets(found, query) or None)) for found in top.scoreDocs] def search_perfect_book(self, searched, max_results=20, fuzzy=False, hint=None): """ @@ -884,7 +1017,7 @@ class Search(IndexStore): self.chain_filters([only_in, self.term_filter(Term('is_book', 'true'))]), max_results) for found in top.scoreDocs: - books.append(SearchResult(self.searcher, found)) + books.append(SearchResult(self, found, how_found="search_perfect_book")) return books def search_book(self, searched, max_results=20, fuzzy=False, hint=None): @@ -910,13 +1043,13 @@ class Search(IndexStore): self.chain_filters([only_in, self.term_filter(Term('is_book', 'true'))]), max_results) for found in top.scoreDocs: - books.append(SearchResult(self.searcher, found)) + books.append(SearchResult(self, found, how_found="search_book")) return books def search_perfect_parts(self, searched, max_results=20, fuzzy=False, hint=None): """ - Search for book parts which containt a phrase perfectly matching (with a slop of 2, default for make_phrase()) + Search for book parts which contains a phrase perfectly matching (with a slop of 2, default for make_phrase()) some part/fragment of the book. """ qrys = [self.make_phrase(self.get_tokens(searched), field=fld, fuzzy=fuzzy) for fld in ['content']] @@ -932,16 +1065,17 @@ class Search(IndexStore): flt]), max_results) for found in top.scoreDocs: - books.append(SearchResult(self.searcher, found, snippets=self.get_snippets(found, q))) + books.append(SearchResult(self, found, snippets=self.get_snippets(found, q), how_found='search_perfect_parts')) return books - def search_everywhere(self, searched, max_results=20, fuzzy=False, hint=None): + def search_everywhere(self, searched, max_results=20, fuzzy=False, hint=None, tokens_cache=None): """ Tries to use search terms to match different fields of book (or its parts). E.g. one word can be an author survey, another be a part of the title, and the rest are some words from third chapter. """ + if tokens_cache is None: tokens_cache = {} books = [] only_in = None @@ -951,8 +1085,8 @@ class Search(IndexStore): # content only query : themes x content q = BooleanQuery() - tokens_pl = self.get_tokens(searched, field='content') - tokens = self.get_tokens(searched, field='SIMPLE') + tokens_pl = self.get_tokens(searched, field='content', cached=tokens_cache) + tokens = self.get_tokens(searched, field='SIMPLE', cached=tokens_cache) # only search in themes when we do not already filter by themes if hint is None or hint.just_search_in(['themes']) != []: @@ -964,7 +1098,7 @@ class Search(IndexStore): topDocs = self.searcher.search(q, only_in, max_results) for found in topDocs.scoreDocs: - books.append(SearchResult(self.searcher, found)) + books.append(SearchResult(self, found, how_found='search_everywhere_themesXcontent', searched=searched)) print "* %s theme x content: %s" % (searched, books[-1]._hits) # query themes/content x author/title/tags @@ -983,7 +1117,7 @@ class Search(IndexStore): topDocs = self.searcher.search(q, only_in, max_results) for found in topDocs.scoreDocs: - books.append(SearchResult(self.searcher, found)) + books.append(SearchResult(self, found, how_found='search_everywhere', searched=searched)) print "* %s scatter search: %s" % (searched, books[-1]._hits) return books @@ -1039,11 +1173,15 @@ class Search(IndexStore): stored = self.searcher.doc(scoreDoc.doc) + position = stored.get('snippets_position') + length = stored.get('snippets_length') + if position is None or length is None: + return None # locate content. snippets = Snippets(stored.get('book_id')).open() try: - text = snippets.get((int(stored.get('snippets_position')), - int(stored.get('snippets_length')))) + text = snippets.get((int(position), + int(length))) finally: snippets.close() @@ -1156,7 +1294,7 @@ class Search(IndexStore): Chains a filter list together """ filters = filter(lambda x: x is not None, filters) - if not filters: + if not filters or filters is []: return None chf = ChainedFilter(JArray('object')(filters, Filter), op) return chf