From 90dec5fd76c33df51828a58802b5e1e6e1df76db Mon Sep 17 00:00:00 2001 From: Edward Date: Thu, 20 Dec 2018 14:13:24 -0200 Subject: [PATCH] Fixes #2055 - Busca Textual (#2179) Fixes #2055 - Busca Textual --- Dockerfile | 5 +- sapl/base/search_indexes.py | 72 +- sapl/materia/views.py | 2 + sapl/norma/views.py | 2 + sapl/settings.py | 21 +- .../materia/materialegislativa_filter.html | 10 +- .../templates/norma/normajuridica_filter.html | 10 +- solr/docker-compose.yml | 61 + .../sapl_configset/conf/lang/stopwords_en.txt | 54 + .../sapl_configset/conf/lang/stopwords_pt.txt | 253 +++ solr/sapl_configset/conf/managed-schema | 573 +++++++ solr/sapl_configset/conf/params.json | 20 + solr/sapl_configset/conf/protwords.txt | 21 + solr/sapl_configset/conf/saplconfigset.zip | Bin 0 -> 30297 bytes solr/sapl_configset/conf/schema.xml | 165 ++ solr/sapl_configset/conf/solrconfig.xml | 1367 +++++++++++++++++ solr/sapl_configset/conf/stopwords.txt | 14 + solr/sapl_configset/conf/synonyms.txt | 29 + solr_api.py | 155 ++ start.sh | 18 +- 20 files changed, 2797 insertions(+), 55 deletions(-) create mode 100644 solr/docker-compose.yml create mode 100644 solr/sapl_configset/conf/lang/stopwords_en.txt create mode 100644 solr/sapl_configset/conf/lang/stopwords_pt.txt create mode 100644 solr/sapl_configset/conf/managed-schema create mode 100644 solr/sapl_configset/conf/params.json create mode 100644 solr/sapl_configset/conf/protwords.txt create mode 100644 solr/sapl_configset/conf/saplconfigset.zip create mode 100644 solr/sapl_configset/conf/schema.xml create mode 100644 solr/sapl_configset/conf/solrconfig.xml create mode 100644 solr/sapl_configset/conf/stopwords.txt create mode 100644 solr/sapl_configset/conf/synonyms.txt create mode 100755 solr_api.py diff --git a/Dockerfile b/Dockerfile index 3f3adc78e..ffb812d6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ FROM alpine:3.8 ENV BUILD_PACKAGES postgresql-dev graphviz-dev graphviz build-base git pkgconfig \ - python3-dev libxml2-dev jpeg-dev libressl-dev libffi-dev libxslt-dev \ - nodejs npm py3-lxml py3-magic postgresql-client poppler-utils antiword vim openssh-client + python3-dev libxml2-dev jpeg-dev libressl-dev libffi-dev libxslt-dev \ + nodejs npm py3-lxml py3-magic postgresql-client poppler-utils antiword \ + curl jq openssh-client vim openssh-client RUN apk update --update-cache && apk upgrade diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index f1ec87ddd..0e0283ba8 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -1,6 +1,4 @@ import os.path -import re -import string import textract import logging @@ -8,6 +6,7 @@ from django.db.models import F, Q, Value from django.db.models.fields import TextField from django.db.models.functions import Concat from django.template import loader +from haystack import connections from haystack.constants import Indexable from haystack.fields import CharField from haystack.indexes import SearchIndex @@ -24,6 +23,7 @@ from sapl.utils import RemoveTag class TextExtractField(CharField): + backend = None logger = logging.getLogger(__name__) def __init__(self, **kwargs): @@ -34,24 +34,20 @@ class TextExtractField(CharField): self.model_attr = (self.model_attr, ) def solr_extraction(self, arquivo): - extracted_data = self._get_backend(None).extract_file_contents( - arquivo)['contents'] - # Remove as tags xml - self.logger.debug("Removendo as tags xml.") - extracted_data = re.sub('<[^>]*>', '', extracted_data) - # Remove tags \t e \n - self.logger.debug("Removendo as \t e \n.") - extracted_data = extracted_data.replace( - '\n', ' ').replace('\t', ' ') - # Remove sinais de pontuação - self.logger.debug("Removendo sinais de pontuação.") - extracted_data = re.sub('[' + string.punctuation + ']', - ' ', extracted_data) - # Remove espaços múltiplos - self.logger.debugger("Removendo espaços múltiplos.") - extracted_data = " ".join(extracted_data.split()) - - return extracted_data + if not self.backend: + self.backend = connections['default'].get_backend() + try: + with open(arquivo.path, 'rb') as f: + content = self.backend.extract_file_contents(f) + if not content or not content['contents']: + return '' + data = content['contents'] + except Exception as e: + print('erro processando arquivo: ' % arquivo.path) + self.logger.error(arquivo.path) + self.logger.error('erro processando arquivo: ' % arquivo.path) + data = '' + return data def whoosh_extraction(self, arquivo): @@ -66,11 +62,11 @@ class TextExtractField(CharField): language='pt-br').decode('utf-8').replace('\n', ' ').replace( '\t', ' ') - def print_error(self, arquivo): - self.logger.error("Erro inesperado processando arquivo: {}".format(arquivo.path)) - msg = 'Erro inesperado processando arquivo: %s' % ( - arquivo.path) - print(msg) + def print_error(self, arquivo, error): + msg = 'Erro inesperado processando arquivo %s erro: %s' % ( + arquivo.path, error) + print(msg, error) + self.logger.error(msg, error) def file_extractor(self, arquivo): if not os.path.exists(arquivo.path) or \ @@ -81,9 +77,9 @@ class TextExtractField(CharField): if SOLR_URL: try: return self.solr_extraction(arquivo) - except Exception as e: - self.logger.error("Erro no arquivo {}. ".format(arquivo.path) + str(e)) - self.print_error(arquivo) + except Exception as err: + print(str(err)) + self.print_error(arquivo, err) # Em ambiente de DEV utiliza-se o Whoosh # Como ele não possui extração, faz-se uso do textract @@ -91,13 +87,13 @@ class TextExtractField(CharField): try: self.logger.debug("Tentando whoosh_extraction no arquivo {}".format(arquivo.path)) return self.whoosh_extraction(arquivo) - except ExtensionNotSupported as e: - self.logger.error("Erro no arquivo {}".format(arquivo.path) + str(e)) - print(str(e)) - except Exception as e2: - self.logger.error(str(e)) - print(str(e2)) self.print_error(arquivo) + except ExtensionNotSupported as err: + print(str(err)) + self.logger.error(str(err)) + except Exception as err: + print(str(err)) + self.print_error(arquivo, str(err)) return '' def ta_extractor(self, value): @@ -133,7 +129,9 @@ class TextExtractField(CharField): value = getattr(obj, attr) if not value: continue - data += getattr(self, func)(value) + data += getattr(self, func)(value) + ' ' + + data = data.replace('\n', ' ') return data @@ -159,6 +157,10 @@ class DocumentoAcessorioIndex(SearchIndex, Indexable): ) ) + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.text.search_index = self + def get_model(self): return self.model diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 578357721..8af8a19e2 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -1810,6 +1810,8 @@ class MateriaLegislativaPesquisaView(FilterView): context['show_results'] = show_results_filter_set(qr) + context['USE_SOLR'] = settings.USE_SOLR if hasattr(settings, 'USE_SOLR') else False + return context diff --git a/sapl/norma/views.py b/sapl/norma/views.py index f7800c42f..0e0ed23e4 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -15,6 +15,7 @@ from django.views.generic import TemplateView, UpdateView from django.views.generic.base import RedirectView from django.views.generic.edit import FormView from django_filters.views import FilterView +from sapl import settings from sapl.base.models import AppConfig from sapl.compilacao.views import IntegracaoTaView from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, @@ -107,6 +108,7 @@ class NormaPesquisaView(FilterView): context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' context['show_results'] = show_results_filter_set(qr) + context['USE_SOLR'] = settings.USE_SOLR if hasattr(settings, 'USE_SOLR') else False return context diff --git a/sapl/settings.py b/sapl/settings.py index d30b4df3c..0d6a452bc 100755 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -100,23 +100,28 @@ INSTALLED_APPS = ( # FTS = Full Text Search # Desabilita a indexação textual até encontramos uma solução para a issue # https://github.com/interlegis/sapl/issues/2055 -#HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.BaseSignalProcessor' +#HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.BaseSignalProcessor' # Disable auto index +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' SEARCH_BACKEND = 'haystack.backends.whoosh_backend.WhooshEngine' SEARCH_URL = ('PATH', PROJECT_DIR.child('whoosh')) -SOLR_URL = config('SOLR_URL', cast=str, default='') -if SOLR_URL: +# SOLR +USE_SOLR = config('USE_SOLR', cast=bool, default=False) +SOLR_URL = config('SOLR_URL', cast=str, default='http://localhost:8983') +SOLR_COLLECTION = config('SOLR_COLLECTION', cast=str, default='sapl') + +if USE_SOLR: SEARCH_BACKEND = 'haystack.backends.solr_backend.SolrEngine' - SEARCH_URL = ('URL', config('SOLR_URL', cast=str)) - # ...or for multicore... - # 'URL': 'http://127.0.0.1:8983/solr/mysite', + SEARCH_URL = ('URL', '{}/solr/{}'.format(SOLR_URL, SOLR_COLLECTION)) +# BATCH_SIZE: default is 1000 if omitted, avoid Too Large Entity Body errors HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': SEARCH_BACKEND, - SEARCH_URL[0]: SEARCH_URL[1] + SEARCH_URL[0]: SEARCH_URL[1], + 'BATCH_SIZE': 500, + 'TIMEOUT': 60, }, } diff --git a/sapl/templates/materia/materialegislativa_filter.html b/sapl/templates/materia/materialegislativa_filter.html index cdd408af3..5ff3eaee0 100644 --- a/sapl/templates/materia/materialegislativa_filter.html +++ b/sapl/templates/materia/materialegislativa_filter.html @@ -3,11 +3,13 @@ {% load crispy_forms_tags %} {% block actions %} +
- + {% if USE_SOLR %} + + Pesquisa Textual + + {% endif %} {% if perms.materia.add_materialegislativa %} diff --git a/sapl/templates/norma/normajuridica_filter.html b/sapl/templates/norma/normajuridica_filter.html index ef668cf36..0c7547661 100644 --- a/sapl/templates/norma/normajuridica_filter.html +++ b/sapl/templates/norma/normajuridica_filter.html @@ -4,11 +4,11 @@ {% block actions %}
- + {% if USE_SOLR %} + + Pesquisa Textual + + {% endif %} {% if perms.norma.add_normajuridica %} diff --git a/solr/docker-compose.yml b/solr/docker-compose.yml new file mode 100644 index 000000000..2f97a7e10 --- /dev/null +++ b/solr/docker-compose.yml @@ -0,0 +1,61 @@ +version: '2' +services: + sapldb: + image: postgres:10.5-alpine + restart: always + environment: + POSTGRES_PASSWORD: sapl + POSTGRES_USER: sapl + POSTGRES_DB: sapl + PGDATA : /var/lib/postgresql/data/ + volumes: + - sapldb_data:/var/lib/postgresql/data/ + ports: + - "5432:5432" + + saplsolr: + image: solr:7.4-alpine + restart: always + command: bin/solr start -c -f + volumes: + - solr_data:/opt/solr/server/solr + - solr_configsets:/opt/solr/server/solr/configsets + ports: + - "8983:8983" + + sapl: + image: interlegis/sapl:3.1.138 + # build: . + restart: always + environment: + ADMIN_PASSWORD: interlegis + ADMIN_EMAIL: email@dominio.net + DEBUG: 'False' + USE_TLS: 'False' + EMAIL_PORT: 587 + EMAIL_HOST: smtp.dominio.net + EMAIL_HOST_USER: usuariosmtp + EMAIL_HOST_PASSWORD: senhasmtp + USE_SOLR: 'True' + #SOLR_COLLECTION: sapl + #SOLR_HOST: saplsolr + SOLR_URL: http://saplsolr:8983/solr/sapl + TZ: America/Sao_Paulo + volumes: + - sapl_data:/var/interlegis/sapl/data + - sapl_media:/var/interlegis/sapl/media + - sapl_root:/var/interlegis/sapl + volumes_from: + - saplsolr + depends_on: + - sapldb + - saplsolr + ports: + - "80:80" +volumes: + sapldb_data: + sapl_data: + sapl_media: + sapl_root: + solr_data: + solr_configsets: diff --git a/solr/sapl_configset/conf/lang/stopwords_en.txt b/solr/sapl_configset/conf/lang/stopwords_en.txt new file mode 100644 index 000000000..2c164c0b2 --- /dev/null +++ b/solr/sapl_configset/conf/lang/stopwords_en.txt @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +# Standard english stop words taken from Lucene's StopAnalyzer +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +such +that +the +their +then +there +these +they +this +to +was +will +with diff --git a/solr/sapl_configset/conf/lang/stopwords_pt.txt b/solr/sapl_configset/conf/lang/stopwords_pt.txt new file mode 100644 index 000000000..acfeb01af --- /dev/null +++ b/solr/sapl_configset/conf/lang/stopwords_pt.txt @@ -0,0 +1,253 @@ + | From svn.tartarus.org/snowball/trunk/website/algorithms/portuguese/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Portuguese stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + + | The following is a ranked list (commonest to rarest) of stopwords + | deriving from a large sample of text. + + | Extra words have been added at the end. + +de | of, from +a | the; to, at; her +o | the; him +que | who, that +e | and +do | de + o +da | de + a +em | in +um | a +para | for + | é from SER +com | with +não | not, no +uma | a +os | the; them +no | em + o +se | himself etc +na | em + a +por | for +mais | more +as | the; them +dos | de + os +como | as, like +mas | but + | foi from SER +ao | a + o +ele | he +das | de + as + | tem from TER +à | a + a +seu | his +sua | her +ou | or + | ser from SER +quando | when +muito | much + | há from HAV +nos | em + os; us +já | already, now + | está from EST +eu | I +também | also +só | only, just +pelo | per + o +pela | per + a +até | up to +isso | that +ela | he +entre | between + | era from SER +depois | after +sem | without +mesmo | same +aos | a + os + | ter from TER +seus | his +quem | whom +nas | em + as +me | me +esse | that +eles | they + | estão from EST +você | you + | tinha from TER + | foram from SER +essa | that +num | em + um +nem | nor +suas | her +meu | my +às | a + as +minha | my + | têm from TER +numa | em + uma +pelos | per + os +elas | they + | havia from HAV + | seja from SER +qual | which + | será from SER +nós | we + | tenho from TER +lhe | to him, her +deles | of them +essas | those +esses | those +pelas | per + as +este | this + | fosse from SER +dele | of him + + | other words. There are many contractions such as naquele = em+aquele, + | mo = me+o, but they are rare. + | Indefinite article plural forms are also rare. + +tu | thou +te | thee +vocês | you (plural) +vos | you +lhes | to them +meus | my +minhas +teu | thy +tua +teus +tuas +nosso | our +nossa +nossos +nossas + +dela | of her +delas | of them + +esta | this +estes | these +estas | these +aquele | that +aquela | that +aqueles | those +aquelas | those +isto | this +aquilo | that + + | forms of estar, to be (not including the infinitive): +estou +está +estamos +estão +estive +esteve +estivemos +estiveram +estava +estávamos +estavam +estivera +estivéramos +esteja +estejamos +estejam +estivesse +estivéssemos +estivessem +estiver +estivermos +estiverem + + | forms of haver, to have (not including the infinitive): +hei +há +havemos +hão +houve +houvemos +houveram +houvera +houvéramos +haja +hajamos +hajam +houvesse +houvéssemos +houvessem +houver +houvermos +houverem +houverei +houverá +houveremos +houverão +houveria +houveríamos +houveriam + + | forms of ser, to be (not including the infinitive): +sou +somos +são +era +éramos +eram +fui +foi +fomos +foram +fora +fôramos +seja +sejamos +sejam +fosse +fôssemos +fossem +for +formos +forem +serei +será +seremos +serão +seria +seríamos +seriam + + | forms of ter, to have (not including the infinitive): +tenho +tem +temos +tém +tinha +tínhamos +tinham +tive +teve +tivemos +tiveram +tivera +tivéramos +tenha +tenhamos +tenham +tivesse +tivéssemos +tivessem +tiver +tivermos +tiverem +terei +terá +teremos +terão +teria +teríamos +teriam diff --git a/solr/sapl_configset/conf/managed-schema b/solr/sapl_configset/conf/managed-schema new file mode 100644 index 000000000..0cba1950a --- /dev/null +++ b/solr/sapl_configset/conf/managed-schema @@ -0,0 +1,573 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/solr/sapl_configset/conf/params.json b/solr/sapl_configset/conf/params.json new file mode 100644 index 000000000..06114ef25 --- /dev/null +++ b/solr/sapl_configset/conf/params.json @@ -0,0 +1,20 @@ +{"params":{ + "query":{ + "defType":"edismax", + "q.alt":"*:*", + "rows":"10", + "fl":"*,score", + "":{"v":0} + }, + "facets":{ + "facet":"on", + "facet.mincount": "1", + "":{"v":0} + }, + "velocity":{ + "wt": "velocity", + "v.template":"browse", + "v.layout": "layout", + "":{"v":0} + } +}} \ No newline at end of file diff --git a/solr/sapl_configset/conf/protwords.txt b/solr/sapl_configset/conf/protwords.txt new file mode 100644 index 000000000..1dfc0abec --- /dev/null +++ b/solr/sapl_configset/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/solr/sapl_configset/conf/saplconfigset.zip b/solr/sapl_configset/conf/saplconfigset.zip new file mode 100644 index 0000000000000000000000000000000000000000..13a7a41ce84f6f9fe4af1f73fd10906b37291d7e GIT binary patch literal 30297 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 z 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 a_5VbK`rFR@4`moUK=kkT9?bbauKpJT4iLEj literal 0 HcmV?d00001 diff --git a/solr/sapl_configset/conf/schema.xml b/solr/sapl_configset/conf/schema.xml new file mode 100644 index 000000000..597033929 --- /dev/null +++ b/solr/sapl_configset/conf/schema.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + text + + + + diff --git a/solr/sapl_configset/conf/solrconfig.xml b/solr/sapl_configset/conf/solrconfig.xml new file mode 100644 index 000000000..9a9f29196 --- /dev/null +++ b/solr/sapl_configset/conf/solrconfig.xml @@ -0,0 +1,1367 @@ + + + + + + + + + 7.3.1 + + + + + + + + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${solr.lock.type:native} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${solr.ulog.dir:} + ${solr.ulog.numVersionBuckets:65536} + + + + + 300000 + false + + + + + + 30000 + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 20 + + + 200 + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + explicit + 10 + text + edismax + + + + + + + + + + + + + + + + explicit + json + true + + + + + + + + explicit + + + + + + _text_ + + + + + + + true + ignored_ + _text_ + + + + + + + + + text_general + + + + + + default + _text_ + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + + + + + + + default + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + + + + true + false + + + terms + + + + + + + + string + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + [^\w-\.] + _ + + + + + + + yyyy-MM-dd'T'HH:mm:ss.SSSZ + yyyy-MM-dd'T'HH:mm:ss,SSSZ + yyyy-MM-dd'T'HH:mm:ss.SSS + yyyy-MM-dd'T'HH:mm:ss,SSS + yyyy-MM-dd'T'HH:mm:ssZ + yyyy-MM-dd'T'HH:mm:ss + yyyy-MM-dd'T'HH:mmZ + yyyy-MM-dd'T'HH:mm + yyyy-MM-dd HH:mm:ss.SSSZ + yyyy-MM-dd HH:mm:ss,SSSZ + yyyy-MM-dd HH:mm:ss.SSS + yyyy-MM-dd HH:mm:ss,SSS + yyyy-MM-dd HH:mm:ssZ + yyyy-MM-dd HH:mm:ss + yyyy-MM-dd HH:mmZ + yyyy-MM-dd HH:mm + yyyy-MM-dd + + + + + java.lang.String + text_general + + *_str + 256 + + + true + + + java.lang.Boolean + booleans + + + java.util.Date + pdates + + + java.lang.Long + java.lang.Integer + plongs + + + java.lang.Number + pdoubles + + + + + + + + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + ${velocity.template.base.dir:} + ${velocity.solr.resource.loader.enabled:true} + ${velocity.params.resource.loader.enabled:false} + + + + + 5 + + + + + + + + + + + + + + diff --git a/solr/sapl_configset/conf/stopwords.txt b/solr/sapl_configset/conf/stopwords.txt new file mode 100644 index 000000000..ae1e83eeb --- /dev/null +++ b/solr/sapl_configset/conf/stopwords.txt @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/solr/sapl_configset/conf/synonyms.txt b/solr/sapl_configset/conf/synonyms.txt new file mode 100644 index 000000000..eab4ee875 --- /dev/null +++ b/solr/sapl_configset/conf/synonyms.txt @@ -0,0 +1,29 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaafoo => aaabar +bbbfoo => bbbfoo bbbbar +cccfoo => cccbar cccbaz +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterGraphFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + diff --git a/solr_api.py b/solr_api.py new file mode 100755 index 000000000..9bec45daa --- /dev/null +++ b/solr_api.py @@ -0,0 +1,155 @@ + +import requests +import subprocess +import sys +import argparse + + +class SolrClient: + + LIST_CONFIGSETS = "{}/solr/admin/configs?action=LIST&omitHeader=true&wt=json" + UPLOAD_CONFIGSET = "{}/solr/admin/configs?action=UPLOAD&name={}&wt=json" + LIST_COLLECTIONS = "{}/solr/admin/collections?action=LIST&wt=json" + STATUS_COLLECTION = "{}/solr/admin/collections?action=CLUSTERSTATUS&collection={}&wt=json" + STATUS_CORE = "{}/admin/cores?action=STATUS&name={}" + EXISTS_COLLECTION = "{}/solr/{}/admin/ping?wt=json" + OPTIMIZE_COLLECTION = "{}/solr/{}/update?optimize=true&wt=json" + CREATE_COLLECTION = "{}/solr/admin/collections?action=CREATE&name={}&collection.configName={}&numShards={}&replicationFactor={}&maxShardsPerNode={}&wt=json" + DELETE_COLLECTION = "{}/solr/admin/collections?action=DELETE&name={}&wt=json" + DELETE_DATA = "{}/solr/{}/update?commitWithin=1000&overwrite=true&wt=json" + QUERY_DATA = "{}/solr/{}/select?q=*:*" + + CONFIGSET_NAME = "sapl_configset" + + def __init__(self, url): + self.url = url + + def get_num_docs(self, collection_name): + final_url = self.QUERY_DATA.format(self.url, collection_name) + res = requests.get(final_url) + dic = res.json() + num_docs = dic["response"]["numFound"] + return num_docs + + def list_collections(self): + req_url = self.LIST_COLLECTIONS.format(self.url) + res = requests.get(req_url) + dic = res.json() + return dic['collections'] + + def exists_collection(self, collection_name): + collections = self.list_collections() + return True if collection_name in collections else False + + def maybe_upload_configset(self, force=False): + req_url = self.LIST_CONFIGSETS.format(self.url) + res = requests.get(req_url) + dic = res.json() + configsets = dic['configSets'] + # UPLOAD configset + if not self.CONFIGSET_NAME in configsets or force: + files = {'file': ('saplconfigset.zip', + open('./solr/sapl_configset/conf/saplconfigset.zip', + 'rb'), + 'application/octet-stream', + {'Expires': '0'})} + + req_url = self.UPLOAD_CONFIGSET.format(self.url, self.CONFIGSET_NAME) + + resp = requests.post(req_url, files=files) + print(resp.content) + else: + print('O %s já presente no servidor, NÃO enviando.' % self.CONFIGSET_NAME) + + def create_collection(self, collection_name, shards=1, replication_factor=1, max_shards_per_node=1): + self.maybe_upload_configset() + req_url = self.CREATE_COLLECTION.format(self.url, + collection_name, + self.CONFIGSET_NAME, + shards, + replication_factor, + max_shards_per_node) + res = requests.post(req_url) + if res.ok: + print("Collection '%s' created succesfully" % collection_name) + else: + print("Error creating collection '%s'" % collection_name) + as_json = res.json() + print("Error %s: %s" % (res.status_code, as_json['error']['msg'])) + return False + return True + + def delete_collection(self, collection_name): + if collection_name == '*': + collections = self.list_collections() + else: + collections = [collection_name] + + for c in collections: + req_url = self.DELETE_COLLECTION.format(self.url, c) + res = requests.post(req_url) + if not res.ok: + print("Error deleting collection '%s'", c) + print("Code {}: {}".format(res.status_code, res.text)) + else: + print("Collection '%s' deleted successfully!" % c) + + def delete_index_data(self, collection_name): + req_url = self.DELETE_DATA.format(self.url, collection_name) + res = requests.post(req_url, + data='*:*', + headers={'Content-Type': 'application/xml'}) + if not res.ok: + print("Error deleting index for collection '%s'", collection_name) + print("Code {}: {}".format(res.status_code, res.text)) + else: + print("Collection '%s' data deleted successfully!" % collection_name) + + num_docs = self.get_num_docs(collection_name) + print("Num docs: %s" % num_docs) + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description='Cria uma collection no Solr') + + # required arguments + parser.add_argument('-u', type=str, metavar='URL', nargs=1, dest='url', + required=True, help='Endereço do servidor Solr na forma http(s)://
[:port]') + parser.add_argument('-c', type=str, metavar='COLLECTION', dest='collection', nargs=1, + required=True, help='Collection Solr a ser criada') + + # optional arguments + parser.add_argument('-s', type=int, dest='shards', nargs='?', + help='Number of shards (default=1)', default=1) + parser.add_argument('-rf', type=int, dest='replication_factor', nargs='?', + help='Replication factor (default=1)', default=1) + parser.add_argument('-ms', type=int, dest='max_shards_per_node', nargs='?', + help='Max shards per node (default=1)', default=1) + + try: + args = parser.parse_args() + except IOError as msg: + parser.error(str(msg)) + sys.exit(-1) + + url = args.url.pop() + collection = args.collection.pop() + + client = SolrClient(url=url) + + if not client.exists_collection(collection): + print("Collection '%s' doesn't exists. Creating a new one..." % collection) + created = client.create_collection(collection, + shards=args.shards, + replication_factor=args.replication_factor, + max_shards_per_node=args.max_shards_per_node) + if not created: + sys.exit(-1) + else: + print("Collection '%s' exists." % collection) + + num_docs = client.get_num_docs(collection) + if num_docs == 0: + print("Performing a full reindex of '%s' collection..." % collection) + p = subprocess.call(["python3", "manage.py", "rebuild_index", "--noinput"]) diff --git a/start.sh b/start.sh index 9695572ef..865c37079 100755 --- a/start.sh +++ b/start.sh @@ -36,6 +36,10 @@ create_env() { echo "EMAIL_SEND_USER = ""${EMAIL_HOST_USER-''}" >> $FILENAME echo "DEFAULT_FROM_EMAIL = ""${EMAIL_HOST_USER-''}" >> $FILENAME echo "SERVER_EMAIL = ""${EMAIL_HOST_USER-''}" >> $FILENAME + echo "USE_SOLR = ""${USER_SOLR-True}" >> $FILENAME + echo "SOLR_COLLECTION = ""${SOLR_COLLECTION-'sapl'}" >> $FILENAME + echo "SOLR_URL = ""${SOLR_URL-'http://saplsolr:8983'}" >> $FILENAME + echo "[ENV FILE] done." } @@ -46,10 +50,22 @@ create_env /bin/sh busy-wait.sh $DATABASE_URL +## SOLR + +NUM_SHARDS=""${NUM_SHARDS-1}" +RF=""${RF-1}" +MAX_SHARDS_PER_NODE=""${NUM_SHARDS-1}" + +# Verifica se a variável USE_SOLR foi definida e é igual a True +if [[ ! -z "$USE_SOLR" ]] && [[ "$USE_SOLR" = "True" ]]; then + python3 solr_api.py -u $SOLR_URL -c $SOLR_COLLECTION -s $NUM_SHARDS -rf $RF -ms $MAX_SHARDS_PER_NODE & + # python3 manage.py rebuild_index --noinput & +fi + # manage.py migrate --noinput nao funcionava yes yes | python3 manage.py migrate #python3 manage.py collectstatic --no-input -# python3 manage.py rebuild_index --noinput & + echo "Criando usuário admin..."