Compare commits

...

87 Commits

Author SHA1 Message Date
Jesse Weaver 29c349c8d5 Bug 12748: (QA followup) make new tests pass
* t/Koha/ItemType.pm:
    * Rename to .t
    * Fix method names
  * t/db_dependent/Koha/ItemTypes.t: Fix method names and Koha::Database usage

Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:32 -03:00
Jesse Weaver 6eec9dc155 Bug 12748: (QA followup) fix several tiny QA tools warnings
* admin/searchengine/elasticsearch/mappings.yaml:
    * Fix indentation so QA tools believe it's valid YAML
  * C4::AuthoritiesMarc: Remove tab character
  * C4::Search: Fix POD
  * Koha::BiblioUtils: remove "return undef"
  * Koha::Schema::Result::Search*: rename type to _type
  * Koha::SE::ES::Search:
    * Fix spelling/@_[0]
    * Add my to loop variable
    * Remove unused var $error
    * Add use Modern::Perl
    * Fix undefined variables
  * Koha::ES::Indexer: fix POD
  * Koha::SE::ES::QueryBuilder:
    * Don't define variables in conditionals
    * Fix POD
  * Koha::SE::Zebra::Search:
    * Fix debug warns/POD/barewords
    * Koha::SE::Zebra::Search: add use Modern::Perl
  * kohastructure.sql: remove old, unused elasticsearch_mappings table,
    add new tables
  * labels/label-item-search.pl: remove C4::Dates
  * opac/elasticsearch.pl: remove old file

Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:26 -03:00
Tomas Cohen Arazi e16f7988c8 Bug 16248: ModZebra doesn't update zebraqueue if ES is enabled
The zebraqueue table should still be populated with updates if ES is enabled, so
the Zebra indexer keeps the Z39.50/SRU indexes up to date.

To test:
- Set SearchEngine = Elasticsearch
- Watch for zebraqueue changes
  $ watch -n 0.5 'echo "SELECT COUNT(*) FROM zebraqueue WHERE done=0" | sudo koha-mysql kohadev'
- Call touch_all_biblios.pl to simulate record changes
  $ sudo koha-shell kohadev ; cd kohaclone
  $ perl misc/maintenance/touch_all_biblios.pl -v
=> FAIL: Notice the watch is not changing the number of records to be indexed.
- Apply the patch
- Call touch_all_biblios.pl to simulate record changes
  $ sudo koha-shell kohadev ; cd kohaclone
  $ perl misc/maintenance/touch_all_biblios.pl -v
=> SUCCESS: The count raises (more than 0) and the zebra indexer picks the updates.
- Sign off

Signed-off-by: Chris <chrisc@catalyst.net.nz>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:21 -03:00
Chris Cormack 6920f2190b Bug 12478 Increasing test Coverage for Koha::SearchEngine::Elasticsearch::Search
Before this patch it was 24.6%, after it is 64%

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:17 -03:00
Chris Cormack e1397be4e7 Bug 12478 Shifting tests and adding copyright headers
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:14 -03:00
Chris Cormack 556d88512c Bug 12478 : Fixing the tests for Koha::SearchEngine::Elasticsearch::Search
perl t/Koha_SearchEngine_Elasticsearch_Search.t

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:10 -03:00
Chris Cormack 215a5df087 Bug 12478 : Fixing the tests in t/Koha_ElasticSearch_Indexer.t
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:06 -03:00
Chris e2abca7a66 Bug 12478: Making sure the syspref is set to Zebra by default
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:02:02 -03:00
Chris 440eddd196 Bug 12478 : Remove the rest of the references to the old mapping tables
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:58 -03:00
Chris 9f31070b9c Bug 12478 : Remove reference to deprecated table elasticsearch_mapping.sql
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:50 -03:00
Tomas Cohen Arazi db669272e6 Bug 12478: (QA followup) Koha::SearchEngine should fallback to Zebra
Signed-off-by: Tomas Cohen Arazi <tomascohen@unc.edu.ar>
Signed-off-by: Chris <chrisc@catalyst.net.nz>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:38 -03:00
Jonathan Druart c8c4cc1d54 Bug 12478: Define simple_search_compat for Zebra
When Zebra is defined as the search engine, the authority search is
broken (Can't locate object method "simple_search_compat" via package
"Koha::SearchEngine::Zebra::Search" at /home/koha/koha/C4/AuthoritiesMarc.pm line 357.)
simple_search_compat is not defined in
Koha::SearchEngine::Zebra::Search, it should be a wrapper for
C4::Search::SimpleSearch

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:34 -03:00
Jonathan Druart 72d338b3ee Bug 12478: Display facet terms ordered by number of occurrences
By default ES returns the facet terms ordered by most used, which makes
sense.

This patch removes resort done in the scripts (catalogue/search.pl and
opac/opac-search.pl) and moves it to the module.

For Zebra it's now done in C4::Search::getRecords, and there is no
change to expect (still alphabetically).

On the Elastic search side, we could imagine to let the library define
the order of the facets. The facet terms are now sorted by most used.

To test easily this change, turn on the displayFacetCount pref.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:30 -03:00
Robin Sheat 15a863fe1d Revert "Bug 12478: Display facet terms ordered by number of occurrences"
This reverts commit cd4905c2969b067476881016d0b03271f0bcc7c8.

This commit caused an error in C4::Search::GetFacets when running in
zebra mode.

Conflicts:
	Koha/SearchEngine/Elasticsearch/Search.pm

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:26 -03:00
Jonathan Druart b4610691c7 Bug 12478: Remove previous mappings file (sql)
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:22 -03:00
Jonathan Druart 776dcbae01 Bug 12478: Manually add the many_to_many relationships
This will create the add_to_* accessors for us.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:18 -03:00
Jonathan Druart 4f60862917 Bug 12478: update the schema
These files have been generated with the
misc/devel/update_dbix_class_files.pl script.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:13 -03:00
Jonathan Druart f242f7f186 Bug 12478: Move mapping attributes to the join table
To make things consistent, the attributes of the mappings (sort,
suggestible, facet) should be moved to the join tables
(search_marc_to_field).
Indeed the search_marc_map could represent a mapping for several search
fields with different attributes. Before this change it was not
possible.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:09 -03:00
Jonathan Druart 82539e8650 Bug 12478: Add Koha::SearchField[s] and Koha::SearchMarcMap[s] classes
This was initially submited on bug 14899, but we will need it here.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:05 -03:00
Jonathan Druart 731559aeff Bug 12478: Add the yaml mappings file
This should facilitate the modification of the mappings.
The yaml format is more readable than sql.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:01:01 -03:00
Robin Sheat bdc3831e09 Bug 12478: "fix" notforloan
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:58 -03:00
Jonathan Druart fe94306f9b Bug 12478: Take the FacetMaxCount pref into account
The system preference FacetMaxCount should work as expected with ES.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:55 -03:00
Jonathan Druart e1d8d0180e Bug 12478: Remove empty limit parameter
If no limit are passed, the url will contain '&limit=' anyway.
It is not necessary and can be avoided easily

Test plan:
1/ Search for a term in your catalogue
2/ Hover over a link in the facet area
3/ The link is
cgi-bin/koha/opac-search.pl?idx=kw&q=your_term&limit=&limit=[...]

With this patch, the empty limit parameter does not appear.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:50 -03:00
Jonathan Druart 4c7cde08fe Bug 12478: Display facet terms ordered by number of occurrences
By default ES returns the facet terms ordered by most used, which makes
sense.

This patch removes resort done in the scripts (catalogue/search.pl and
opac/opac-search.pl) and moves it to the module.

For Zebra it's now done in C4::Search::getRecords, and there is no
change to expect (still alphabetically).

On the Elastic search side, we could imagine to let the library define
the order of the facets. The facet terms are now sorted by most used.

To test easily this change, turn on the displayFacetCount pref.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:46 -03:00
Jonathan Druart f5747ddc45 Bug 12478: facets - Display description instead of code for locations
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:41 -03:00
Jonathan Druart 8af5057e19 Bug 12478: Add Koha::AuthorisedValue[s] class
These 2 classes come from bug 10363.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:38 -03:00
Jonathan Druart dca617891f Bug 12478: Use the new Koha::ItemTypes to retrieve itypes descriptions
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:32 -03:00
Jonathan Druart 2d6b64ae6a Bug 12478: Add Koha::ItemType[s] classes
These 2 classes come from bug 14828.
Tests in t/db_dependent/Koha/ItemTypes.t should be updated.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:28 -03:00
Jonathan Druart bcf38792e3 Bug 12478: (follow-up) Display the correct number of facets
This patch fixes a side-effect of the previous "Display the correct
number of facets (5 instead of 6)" patch.

If you ask for more facets ('show more' link) and that the number of
facets is > 5 but < 10, 1 facet will be missing.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:19 -03:00
Robin Sheat f61f339167 Bug 12478: abort early if there's no elasticserch definition
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:16 -03:00
Robin Sheat 412fad80cc Bug 14899: schema changes to make the database better
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:13 -03:00
Jonathan Druart a1ae514e43 Bug 14899: DB changes
This patch applies several changes to the DB structure introduced by
the previous ES patch set:

1/ Add a search_field.label column
=> It will be easier to understand what the search_field does.
Indeed, some are not user friendly: acqdate, an, pl, ff8-23, ln, etc.
TODO later: Update the labels with correct values (at the moment
label=name)

2/ Update the foreign key search_marc_to_field.search_marc_map_id with
on delete cascase. This will permit to automatically remove the entries
in search_marc_to_field when they are deleted in search_marc_map.

3/ Remove the index_name_2 unique constraint on the search_marc_map table.
I don't understand how this could be useful, it was defined like (index_name,
marc_type, marc_field), so it means a mapping cannot be defined twice with
different values for facet, suggestible and sort.
This limitation does not seem to make sense.
Robin, please correct me if I am wrong :)

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:07 -03:00
Robin Sheat fa6f77cc71 Bug 12478: add location, su-geo, se to facets
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:04 -03:00
Robin Sheat 3949b628f4 Bug 12478: fix errors from rebase and new upstream version
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 10:00:01 -03:00
Robin Sheat c6d59cb756 Bug 12478: fix some issues on rebase
There were rebase conflicts that it was just easier to postpone until
afterwards.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:57 -03:00
Jonathan Druart d1621fee0b Bug 12478: Fix error on indexing a specific record
% perl misc/search_tools/rebuild_elastic_search.pl -bn 42
Can't locate object method "idnumber" via package "MARC::Record" at
misc/search_tools/rebuild_elastic_search.pl line 171.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:54 -03:00
Jonathan Druart 0b392ab179 Bug 12478: Fix encoding issue on facets
Filer on "Ślez, Ts." => Can't escape \x{015A}, try uri_escape_utf8()
instead at
/home/koha/src/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm line 221.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:50 -03:00
Jonathan Druart df1c222537 Bug 12478: Do not display the 'Show more' links if no more facet available
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:43 -03:00
Jonathan Druart 9f3c45d62c Bug 12478: Display the correct number of facets (5 instead of 6)
The loop on facet entries was wrong, it should stop after 5
iterations.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:40 -03:00
Jonathan Druart fa447bdc80 Bug 12478: Remove Solr occurrences reintroduced by a previous patch
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:36 -03:00
Jonathan Druart 24d3482fee Bug 12478: Fix pod in the rebuild_ES.pl script
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:33 -03:00
Jonathan Druart c6b997e1d8 Bug 12478: Fix the verbose flag on reindexing
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:59:28 -03:00
Jonathan Druart 9f45e2ee95 Bug 12478: Change the commit count to 5k
It will improve the indexing time.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:31 -03:00
Jonathan Druart 03e000bf30 Bug 12478: Fix the UNIMARC and NORMARC indexing
1/ This patch fixes a major issue on UNIMARC and NORMARC installations
(Compulsive c/p on insert into insert into search_marc_to_field for unimarc and normarc)

2/ After fixing that, I got
invalid marc path at /usr/share/perl5/Catmandu/Fix/marc_map.pm line 35.
on reindexing.
It was caused by some "" values instead of NULL.

3/ Then, a "duplicate entry" error was caused by 3 lines for Local-number.
It seems to be an error, 1 is enough (I suppose 0909 was an error too).

Note that the following is not fixed by this patch:
MariaDB [koha_es_unimarc]>  insert into search_field (name, type) select distinct mapping, type from elasticsearch_mapping;
Query OK, 73 rows affected, 57 warnings (0.05 sec)
Records: 73  Duplicates: 0  Warnings: 57

MariaDB [koha_es_unimarc]> show warnings;
+---------+------+--------------------------------------------+
| Level   | Code | Message                                    |
+---------+------+--------------------------------------------+
| Warning | 1265 | Data truncated for column 'type' at row 1  |

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:24 -03:00
Robin Sheat b5b036e6e9 Bug 12478: allow more granular sorting configuration
This allows sorting to be configured within a field. For example, while
many values are included for search on author, sorting should only be
done on the main entry values. This permits that by have a sort value,
which can be true, false, or null. true and null are pretty much the
same, but false means that a field isn't available for sorting on. By
default (null), fields can be sorted on.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:20 -03:00
Robin Sheat 7a09c1f484 Bug 12478: pull the SQL updates from the browse interface
Hopefully will reduce the rate of conflicts down the road. We'll see.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:17 -03:00
Robin Sheat e293faa0ee Bug 12478: pull the SQL updates from the browse interface
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:13 -03:00
Robin Sheat 9f3703e58b Bug 12478: the facet "show more" links work properly now
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:10 -03:00
Robin Sheat 0d0c8cd2c8 Bug 12478: add the lenient option to the query_string query
It doesn't really help.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:06 -03:00
Robin Sheat 5fbdfd48c0 Bug 12478: improve error reporting a bit
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:58:02 -03:00
Robin Sheat 6e4aa91568 Bug 12478: update to support new Cat::Store::ES
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:58 -03:00
Robin Sheat 3009c4afc7 Bug 12478: ES is now updated when records are updated/deleted
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:55 -03:00
Robin Sheat 6bd544eadb Bug 12478: fix capitalisation issue with index names
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:51 -03:00
Robin Sheat 66cdeac96c Bug 12478: make things using SimpleSearch use the new version
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:47 -03:00
Robin Sheat 951e1dc3ae Bug 12478: a replacement to the SimpleSearch interface implemented
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:43 -03:00
Robin Sheat b358406130 Bug 12478: fix issue with authority sort order being weird
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:39 -03:00
Robin Sheat 9fd04957ed Bug 12478: auth search works in the staff client
Some issues with sort order though.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:36 -03:00
Robin Sheat eaa3bdb7ff Bug 12478: starting authority search in staff client
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:32 -03:00
Robin Sheat 7366a4c0ab Bug 12478: fix issues with the conversion SQL
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:27 -03:00
Robin Sheat 4393c233d6 Bug 12478: change the schema for storing mappings
This makes the mapping schema a proper many-to-many relational
structure, which is a little more fiddly to work with, but will make
things like editors for it easier to write.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:23 -03:00
Robin Sheat f0d4afac61 Bug 12478: fix multi-choice stuff in advanced search
This means that things like itype get "OR"ed together, rather than
"AND"ed like other things.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:18 -03:00
Robin Sheat 45b67a9d2d Bug 12478: starting to add search to staff client
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:14 -03:00
Robin Sheat eee91ee499 Bug 12478: fix wrong counts/results for authority searches
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:10 -03:00
Robin Sheat bcb9bf36b6 Bug 12478: fix issues caused by rebasing
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:57:06 -03:00
Robin Sheat dc63f75605 Bug 12478: reinstate zebra compatibility for auth search
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:56:55 -03:00
Robin Sheat 19a82bb7c2 Bug 12478: paging is fixed now too
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:56:47 -03:00
Robin Sheat b08b8aa5c0 Bug 12478: sort is sorted
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:48 -03:00
Robin Sheat befd95ca7b Bug 12478: phrase searching for authorities is happier
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:40 -03:00
Robin Sheat a141979553 Bug 12478: fix the way counting is done
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:36 -03:00
Robin Sheat 51f4f02fad Bug 12478: authority paging works
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:32 -03:00
Robin Sheat 19e2b47e6b Bug 12478: working on authority results
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:28 -03:00
Robin Sheat 712bbb08dd Bug 12478: make authid get to the templates properly
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:25 -03:00
Robin Sheat fe1e13085e Bug 12478: basic auth search and display is working
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:21 -03:00
Robin Sheat 010a9b2b72 Bug 12478: more authorities
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:17 -03:00
Robin Sheat a690af7ef9 Bug 12478 - more authorites
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:13 -03:00
Robin Sheat 8be6a50fa8 Bug 12478 - more authority searching
Queries are being built, but they seem to be wrong as no results are
returned.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:55:08 -03:00
Robin Sheat 8bddbdcb97 Bug 12478 - more authority query building
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:36 -03:00
Robin Sheat 53309570b0 Bug 12478 - fix syntax errors so that stuff runs
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:32 -03:00
Robin Sheat 233e843ef4 Bug 12478 - authority work in progress
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:28 -03:00
Robin Sheat 9d9b7bc38b Bug 12478 - fix issue with class loading
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:24 -03:00
Robin Sheat 49c976a6c1 Bug 12478 - fix error in configuration loading
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:20 -03:00
Robin Sheat 4d34d4a929 Bug 12478 - fix error in SQL
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:14 -03:00
Robin Sheat de084f7087 Bug 12478 - add test cases
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:08 -03:00
Robin Sheat d9cdde9d27 Bug 12478 - authorities can now be stored in ES
(Not fetched yet though.)

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:53:02 -03:00
Robin Sheat 45103d3745 Bug 12478 - set up database tables for elasticsearch
This sets up the database changes needed to run ES. This isn't intended
to be ready for upstreaming, just to make testing easier.

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:52:56 -03:00
Robin Sheat 036f273d79 Bug 12478 - pile of elasticsearch code
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:52:52 -03:00
Robin Sheat ed70f11e84 Bug 12478 - add some base objects that the ES code will depend on
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Jesse Weaver <jweaver@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
2016-04-26 09:52:45 -03:00
60 changed files with 6849 additions and 101 deletions

View File

@ -29,6 +29,9 @@ use C4::Log;
use Koha::MetadataRecord::Authority;
use Koha::Authorities;
use Koha::Authority::Types;
use Koha::Authority;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
use vars qw(@ISA @EXPORT);
@ -349,7 +352,10 @@ sub CountUsage {
### ZOOM search here
my $query;
$query= "an:".$authid;
my ($err,$res,$result) = C4::Search::SimpleSearch($query,0,10);
# Should really be replaced with a real count call, this is a
# bad way.
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my ($err,$res,$result) = $searcher->simple_search_compat($query,0,1);
if ($err) {
warn "Error: $err from search $query";
$result = 0;
@ -822,7 +828,8 @@ sub FindDuplicateAuthority {
$_->[1]=~s/$filtervalues/ /g; $query.= " $op he:\"".$_->[1]."\"" if ($_->[0]=~/[A-z]/);
}
}
my ($error, $results, $total_hits) = C4::Search::SimpleSearch( $query, 0, 1, [ "authorityserver" ] );
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::AUTHORITIES_INDEX});
my ($error, $results, $total_hits) = $searcher->simple_search_compat( $query, 0, 1 );
# there is at least 1 result => return the 1st one
if (!defined $error && @{$results} ) {
my $marcrecord = C4::Search::new_record_from_zebra(

View File

@ -37,12 +37,15 @@ use C4::ClassSource;
use C4::Charset;
use C4::Linker;
use C4::OAI::Sets;
use C4::Debug;
use Koha::Cache;
use Koha::Authority::Types;
use Koha::Acquisition::Currencies;
use Koha::SearchEngine;
use vars qw(@ISA @EXPORT);
use vars qw($debug $cgi_debug);
BEGIN {
@ -2888,31 +2891,60 @@ sub TransformMarcToKohaOneField {
=head2 ModZebra
ModZebra( $biblionumber, $op, $server );
ModZebra( $biblionumber, $op, $server, $record );
$biblionumber is the biblionumber we want to index
$op is specialUpdate or delete, and is used to know what we want to do
$op is specialUpdate or recordDelete, and is used to know what we want to do
$server is the server that we want to update
$record is the update MARC record if it's available. If it's not supplied
and is needed, it'll be loaded from the database.
=cut
sub ModZebra {
###Accepts a $server variable thus we can use it for biblios authorities or other zebra dbs
my ( $biblionumber, $op, $server ) = @_;
my ( $biblionumber, $op, $server, $record ) = @_;
$debug && warn "ModZebra: update requested for: $biblionumber $op $server\n";
if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
# TODO abstract to a standard API that'll work for whatever
require Koha::ElasticSearch::Indexer;
my $indexer = Koha::ElasticSearch::Indexer->new(
{
index => $server eq 'biblioserver'
? $Koha::SearchEngine::BIBLIOS_INDEX
: $Koha::SearchEngine::AUTHORITIES_INDEX
}
);
if ( $op eq 'specialUpdate' ) {
unless ($record) {
$record = GetMarcBiblio($biblionumber, 1);
}
my $records = [$record];
$indexer->update_index_background( [$biblionumber], [$record] );
}
elsif ( $op eq 'recordDelete' ) {
$indexer->delete_index_background( [$biblionumber] );
}
else {
croak "ModZebra called with unknown operation: $op";
}
}
my $dbh = C4::Context->dbh;
# true ModZebra commented until indexdata fixes zebraDB crashes (it seems they occur on multiple updates
# at the same time
# replaced by a zebraqueue table, that is filled with ModZebra to run.
# the table is emptied by rebuild_zebra.pl script (using the -z switch)
my $check_sql = "SELECT COUNT(*) FROM zebraqueue
WHERE server = ?
AND biblio_auth_number = ?
AND operation = ?
AND done = 0";
WHERE server = ?
AND biblio_auth_number = ?
AND operation = ?
AND done = 0";
my $check_sth = $dbh->prepare_cached($check_sql);
$check_sth->execute( $server, $biblionumber, $op );
my ($count) = $check_sth->fetchrow_array;
@ -3468,7 +3500,7 @@ sub ModBiblioMarc {
$sth = $dbh->prepare("UPDATE biblioitems SET marc=?,marcxml=? WHERE biblionumber=?");
$sth->execute( $record->as_usmarc(), $record->as_xml_record($encoding), $biblionumber );
$sth->finish;
ModZebra( $biblionumber, "specialUpdate", "biblioserver" );
ModZebra( $biblionumber, "specialUpdate", "biblioserver", $record );
return $biblionumber;
}

View File

@ -38,6 +38,8 @@ use Koha::DateUtils qw/dt_from_string/;
use Koha::Database;
use Koha::Database;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
use vars qw(@ISA @EXPORT);
@ -2601,12 +2603,12 @@ counts Usage of itemnumber in Analytical bibliorecords.
sub GetAnalyticsCount {
my ($itemnumber) = @_;
require C4::Search;
### ZOOM search here
my $query;
$query= "hi=".$itemnumber;
my ($err,$res,$result) = C4::Search::SimpleSearch($query,0,10);
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my ($err,$res,$result) = $searcher->simple_search_compat($query,0,10);
return ($result);
}

View File

@ -23,7 +23,8 @@ use warnings;
use C4::Context;
use MARC::Record;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
=head1 NAME
@ -655,10 +656,9 @@ sub get_matches {
#NOTE: double-quote the values so you don't get a "Embedded truncation not supported" error when a term has a ? in it.
}
require C4::Search;
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
( $error, $searchresults, $total_hits ) =
C4::Search::SimpleSearch( $query, 0, $max_matches );
$searcher->simple_search_compat( $query, 0, $max_matches );
}
elsif ( $self->{'record_type'} eq 'authority' ) {
my $authresults;

View File

@ -623,6 +623,15 @@ sub getRecords {
}
}
);
# This sorts the facets into alphabetical order
if (@facets_loop) {
foreach my $f (@facets_loop) {
$f->{facets} = [ sort { uc($a->{facet_label_value}) cmp uc($b->{facet_label_value}) } @{ $f->{facets} } ];
}
@facets_loop = sort {$a->{expand} cmp $b->{expand}} @facets_loop;
}
return ( undef, $results_hashref, \@facets_loop );
}
@ -1321,6 +1330,7 @@ Shim function to ease the transition from buildQuery to a new QueryParser.
This function is called at the beginning of buildQuery, and modifies
buildQuery's input. If it can handle the input, it returns a query that
buildQuery will not try to parse.
=cut
sub parseQuery {
@ -2538,7 +2548,7 @@ sub _ZOOM_event_loop {
=head2 new_record_from_zebra
Given raw data from a Zebra result set, return a MARC::Record object
Given raw data from a searchengine result set, return a MARC::Record object
This helper function is needed to take into account all the involved
system preferences and configuration variables to properly create the
@ -2547,6 +2557,9 @@ MARC::Record object.
If we are using GRS-1, then the raw data we get from Zebra should be USMARC
data. If we are using DOM, then it has to be MARCXML.
If we are using elasticsearch, it'll already be a MARC::Record and this
function needs a new name.
=cut
sub new_record_from_zebra {
@ -2554,6 +2567,10 @@ sub new_record_from_zebra {
my $server = shift;
my $raw_data = shift;
# Set the default indexing modes
my $search_engine = C4::Context->preference("SearchEngine");
if ($search_engine eq 'Elasticsearch') {
return $raw_data;
}
my $index_mode = ( $server eq 'biblioserver' )
? C4::Context->config('zebra_bib_index_mode') // 'dom'
: C4::Context->config('zebra_auth_index_mode') // 'dom';

View File

@ -26,6 +26,9 @@ use C4::External::Syndetics qw(get_syndetics_editions);
use LWP::UserAgent;
use HTTP::Request::Common;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
use strict;
#use warnings; FIXME - Bug 2505
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
@ -63,7 +66,8 @@ sub _get_biblio_from_xisbn {
my $xisbn = shift;
my $dbh = C4::Context->dbh;
my ( $errors, $results, $total_hits ) = C4::Search::SimpleSearch( "nb=$xisbn", 0, 1 );
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( "nb=$xisbn", 0, 1 );
return unless ( !$errors && scalar @$results );
my $record = C4::Search::new_record_from_zebra( 'biblioserver', $results->[0] );

View File

@ -27,7 +27,7 @@ use base qw(Koha::Object);
=head1 NAME
Koha::AuthorisedValue - Koha Authorised Value Object class
Koha::AuthorisedValue - Koha Authorised value Object class
=head1 API

View File

@ -29,7 +29,7 @@ use base qw(Koha::Objects);
=head1 NAME
Koha::AuthorisedValues - Koha AuthorisedValue Object set class
Koha::AuthorisedValues - Koha Authorised value Object set class
=head1 API

View File

@ -22,6 +22,8 @@ use Modern::Perl;
use Carp;
use Koha::Database;
use C4::Context;
use MARC::Record;
use base qw(Koha::Object);
@ -43,4 +45,54 @@ sub _type {
return 'AuthHeader';
}
=head2 get_all_authorities_iterator
my $it = Koha::Authority->get_all_authorities_iterator();
This will provide an iterator object that will, one by one, provide the
Koha::Authority of each authority.
The iterator is a Koha::MetadataIterator object.
=cut
sub get_all_authorities_iterator {
my $database = Koha::Database->new();
my $schema = $database->schema();
my $rs =
$schema->resultset('AuthHeader')->search( { marcxml => { '!=', undef } },
{ columns => [qw/ authid authtypecode marcxml /] } );
my $next_func = sub {
my $row = $rs->next();
return undef if !$row;
my $authid = $row->authid;
my $authtypecode = $row->authtypecode;
my $marcxml = $row->marcxml;
my $record = eval {
MARC::Record->new_from_xml(
StripNonXmlChars($marcxml),
'UTF-8',
(
C4::Context->preference("marcflavour") eq "UNIMARC"
? "UNIMARCAUTH"
: C4::Context->preference("marcflavour")
)
);
};
confess $@ if ($@);
$record->encoding('UTF-8');
# I'm not sure why we don't use the authtypecode from the database,
# but this is how the original code does it.
require C4::AuthoritiesMarc;
$authtypecode = C4::AuthoritiesMarc::GuessAuthTypeCode($record);
my $auth = __PACKAGE__->new( $record, $authid, $authtypecode );
return $auth;
};
return Koha::MetadataIterator->new($next_func);
}
1;

164
Koha/BiblioUtils.pm Normal file
View File

@ -0,0 +1,164 @@
package Koha::BiblioUtils;
# This contains functions to do with managing biblio records.
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
Koha::BiblioUtils - contains fundamental biblio-related functions
=head1 DESCRIPTION
This contains functions for normal operations on biblio records.
Note: really, C4::Biblio does the main functions, but the Koha namespace is
the new thing that should be used.
=cut
use C4::Biblio; # EmbedItemsInMarcBiblio
use Koha::MetadataIterator;
use Koha::Database;
use Modern::Perl;
use Data::Dumper; # TODO remove
use base qw(Koha::MetadataRecord);
__PACKAGE__->mk_accessors(qw( record schema id datatype ));
=head1 FUNCTIONS
=head2 new
my $biblio = Koha::BiblioUtils->new($marc_record, [$biblionumber]);
Creates an instance of C<Koha::BiblioUtils> based on the marc record. If known,
the biblionumber can be provided too.
=cut
sub new {
my $class = shift;
my $record = shift;
my $biblionumber = shift;
my $self = $class->SUPER::new(
{
'record' => $record,
'schema' => lc C4::Context->preference("marcflavour"),
'id' => $biblionumber,
'datatype' => 'biblio',
}
);
bless $self, $class;
return $self;
}
=head2 get_from_biblionumber
my $biblio = Koha::BiblioUtils->get_from_biblionumber($biblionumber, %options);
This will give you an instance of L<Koha::BiblioUtils> that is the biblio that
you requested.
Options are:
=over 4
=item C<$item_data>
If true, then the item data will be merged into the record when it's loaded.
=back
It will return C<undef> if the biblio doesn't exist.
=cut
sub get_from_biblionumber {
my ($class, $bibnum, %options) = @_;
my $marc = $class->get_marc_biblio($bibnum, %options);
return $class->new($marc, $bibnum);
}
=head2 get_all_biblios_iterator
my $it = Koha::BiblioUtils->get_all_biblios_iterator();
This will provide an iterator object that will, one by one, provide the
Koha::BiblioUtils of each biblio. This will include the item data.
The iterator is a Koha::MetadataIterator object.
=cut
sub get_all_biblios_iterator {
my $database = Koha::Database->new();
my $schema = $database->schema();
my $rs =
$schema->resultset('Biblio')->search( {},
{ columns => [qw/ biblionumber /] } );
my $next_func = sub {
# Warn and skip bad records, otherwise we break the loop
while (1) {
my $row = $rs->next();
return if !$row;
my $marc = C4::Biblio::GetMarcBiblio( $row->biblionumber, 1 );
my $next = eval {
__PACKAGE__->new($marc, $row->biblionumber);
};
if ($@) {
warn "Something went wrong reading record for biblio $row->biblionumber: $@\n";
next;
}
return $next;
}
};
return Koha::MetadataIterator->new($next_func);
}
=head2 get_marc_biblio
my $marc = Koha::BiblioUtils->get_marc_biblio($bibnum, %options);
This non-class function fetches the MARC::Record for the given biblio number.
Nothing is returned if the biblionumber couldn't be found (or it somehow has no
MARC data.)
Options are:
=over 4
=item item_data
If set to true, item data is embedded in the record. Default is to not do this.
=back
=cut
sub get_marc_biblio {
my ($class, $bibnum, %options) = @_;
return C4::Biblio::GetMarcBiblio( $bibnum, ($options{item_data} ? 1 : 0 ) );
}
1;

View File

@ -0,0 +1,126 @@
package Koha::BiblioUtils::Iterator;
# This contains an iterator over biblio records
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
Koha::BiblioUtils::Iterator - iterates over biblios provided by a DBIx::Class::ResultSet
=head1 DESCRIPTION
This provides an iterator that gives the MARC::Record of each biblio that's
returned by a L<DBIx::Class::ResultSet> that provides a C<biblionumber>, and
C<marc> or C<marcxml> column from the biblioitems table.
=head1 SYNOPSIS
use Koha::BiblioUtils::Iterator;
my $rs = $schema->resultset('biblioitems');
my $iterator = Koha::BiblioUtils::Iterator->new($rs);
while (my $record = $iterator->next()) {
// do something with $record
}
=head1 METHODS
=cut
use C4::Biblio; # :( - for EmbedItemsInMarcBiblio
use Carp;
use MARC::Record;
use MARC::File::XML;
use Modern::Perl;
=head2 new
my $it = new($sth, option => $value, ...);
Takes a ResultSet to iterate over, and gives you an iterator on it. Optional
options may be specified.
=head3 Options
=over 4
=item items
Set to true to include item data in the resulting MARC record.
=back
=cut
sub new {
my ( $class, $rs, %options ) = @_;
bless {
rs => $rs,
%options,
}, $class;
}
=head2 next()
In a scalar context, provides the next MARC::Record from the ResultSet, or
C<undef> if there are no more.
In a list context it will provide ($biblionumber, $record).
=cut
sub next {
my ($self) = @_;
my $marc;
my $row = $self->{rs}->next();
return if !$row;
if ( $row->marc ) {
$marc = MARC::Record->new_from_usmarc( $row->marc );
}
elsif ( $row->marcxml ) {
$marc = MARC::Record->new_from_xml( $row->marcxml );
}
else {
confess "No marc or marcxml column returned in the request.";
}
my $bibnum;
if ( $self->{items} ) {
$bibnum = $row->get_column('biblionumber');
confess "No biblionumber column returned in the request."
if ( !defined($bibnum) );
# TODO this should really be in Koha::BiblioUtils or something similar.
C4::Biblio::EmbedItemsInMarcBiblio( $marc, $bibnum );
}
if (wantarray) {
$bibnum //= $row->get_column('biblionumber');
confess "No biblionumber column returned in the request."
if ( !defined($bibnum) );
return ( $bibnum, $marc );
}
else {
return $marc;
}
}
1;

View File

@ -41,6 +41,8 @@ use vars qw($database);
__PACKAGE__->mk_accessors(qw( ));
our $schema; # the schema is a singleton
# _new_schema
# Internal helper function (not a method!). This creates a new
# database connection from the data given in the current context, and
@ -110,8 +112,7 @@ creates one, and connects to the database.
This database handle is cached for future use: if you call
C<$database-E<gt>schema> twice, you will get the same handle both
times. If you need a second database handle, use C<&new_schema> and
possibly C<&set_schema>.
times. If you need a second database handle, use C<&new_schema>.
=cut

450
Koha/ElasticSearch.pm Normal file
View File

@ -0,0 +1,450 @@
package Koha::ElasticSearch;
# Copyright 2015 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use base qw(Class::Accessor);
use C4::Context;
use Koha::Database;
use Carp;
use JSON;
use Modern::Perl;
use Readonly;
use Data::Dumper; # TODO remove
__PACKAGE__->mk_ro_accessors(qw( index ));
__PACKAGE__->mk_accessors(qw( sort_fields ));
# Constants to refer to the standard index names
Readonly our $BIBLIOS_INDEX => 'biblios';
Readonly our $AUTHORITIES_INDEX => 'authorities';
=head1 NAME
Koha::ElasticSearch - Base module for things using elasticsearch
=head1 ACCESSORS
=over 4
=item index
The name of the index to use, generally 'biblios' or 'authorities'.
=back
=head1 FUNCTIONS
=cut
sub new {
my $class = shift @_;
my $self = $class->SUPER::new(@_);
# Check for a valid index
croak('No index name provided') unless $self->index;
return $self;
}
=head2 get_elasticsearch_params
my $params = $self->get_elasticsearch_params();
This provides a hashref that contains the parameters for connecting to the
ElasicSearch servers, in the form:
{
'nodes' => ['127.0.0.1:9200', 'anotherserver:9200'],
'index_name' => 'koha_instance_index',
}
This is configured by the following in the C<config> block in koha-conf.xml:
<elasticsearch>
<server>127.0.0.1:9200</server>
<server>anotherserver:9200</server>
<index_name>koha_instance</index_name>
</elasticsearch>
=cut
sub get_elasticsearch_params {
my ($self) = @_;
# Copy the hash so that we're not modifying the original
my $conf = C4::Context->config('elasticsearch');
die "No 'elasticsearch' block is defined in koha-conf.xml.\n" if ( !$conf );
my $es = { %{ $conf } };
# Helpfully, the multiple server lines end up in an array for us anyway
# if there are multiple ones, but not if there's only one.
my $server = $es->{server};
delete $es->{server};
if ( ref($server) eq 'ARRAY' ) {
# store it called 'nodes' (which is used by newer Search::Elasticsearch)
$es->{nodes} = $server;
}
elsif ($server) {
$es->{nodes} = [$server];
}
else {
die "No elasticsearch servers were specified in koha-conf.xml.\n";
}
die "No elasticserver index_name was specified in koha-conf.xml.\n"
if ( !$es->{index_name} );
# Append the name of this particular index to our namespace
$es->{index_name} .= '_' . $self->index;
return $es;
}
=head2 get_elasticsearch_settings
my $settings = $self->get_elasticsearch_settings();
This provides the settings provided to elasticsearch when an index is created.
These can do things like define tokenisation methods.
A hashref containing the settings is returned.
=cut
sub get_elasticsearch_settings {
my ($self) = @_;
# Ultimately this should come from a file or something, and not be
# hardcoded.
my $settings = {
index => {
analysis => {
analyzer => {
analyser_phrase => {
tokenizer => 'keyword',
filter => ['lowercase'],
},
analyser_standard => {
tokenizer => 'standard',
filter => ['lowercase'],
}
},
}
}
};
return $settings;
}
=head2 get_elasticsearch_mappings
my $mappings = $self->get_elasticsearch_mappings();
This provides the mappings that get passed to elasticsearch when an index is
created.
=cut
sub get_elasticsearch_mappings {
my ($self) = @_;
# TODO cache in the object?
my $mappings = {
data => {
properties => {
record => {
store => "yes",
include_in_all => JSON::false,
type => "string",
},
'_all.phrase' => {
search_analyzer => "analyser_phrase",
index_analyzer => "analyser_phrase",
type => "string",
},
}
}
};
my %sort_fields;
my $marcflavour = lc C4::Context->preference('marcflavour');
$self->_foreach_mapping(
sub {
my ( $name, $type, $facet, $suggestible, $sort, $marc_type ) = @_;
return if $marc_type ne $marcflavour;
# TODO if this gets any sort of complexity to it, it should
# be broken out into its own function.
# TODO be aware of date formats, but this requires pre-parsing
# as ES will simply reject anything with an invalid date.
my $es_type =
$type eq 'boolean'
? 'boolean'
: 'string';
$mappings->{data}{properties}{$name} = {
search_analyzer => "analyser_standard",
index_analyzer => "analyser_standard",
type => $es_type,
fields => {
phrase => {
search_analyzer => "analyser_phrase",
index_analyzer => "analyser_phrase",
type => "string",
copy_to => "_all.phrase",
},
raw => {
"type" => "string",
"index" => "not_analyzed",
}
},
};
$mappings->{data}{properties}{$name}{null_value} = 0
if $type eq 'boolean';
if ($facet) {
$mappings->{data}{properties}{ $name . '__facet' } = {
type => "string",
index => "not_analyzed",
};
}
if ($suggestible) {
$mappings->{data}{properties}{ $name . '__suggestion' } = {
type => 'completion',
index_analyzer => 'simple',
search_analyzer => 'simple',
};
}
# Sort may be true, false, or undef. Here we care if it's
# anything other than undef.
if (defined $sort) {
$mappings->{data}{properties}{ $name . '__sort' } = {
search_analyzer => "analyser_phrase",
index_analyzer => "analyser_phrase",
type => "string",
include_in_all => JSON::false,
fields => {
phrase => {
search_analyzer => "analyser_phrase",
index_analyzer => "analyser_phrase",
type => "string",
},
},
};
$sort_fields{$name} = 1;
}
}
);
$self->sort_fields(\%sort_fields);
return $mappings;
}
# This overrides the accessor provided by Class::Accessor so that if
# sort_fields isn't set, then it'll generate it.
sub sort_fields {
my $self = shift;
if (@_) {
$self->_sort_fields_accessor(@_);
return;
}
my $val = $self->_sort_fields_accessor();
return $val if $val;
# This will populate the accessor as a side effect
$self->get_elasticsearch_mappings();
return $self->_sort_fields_accessor();
}
# Provides the rules for data conversion.
sub get_fixer_rules {
my ($self) = @_;
my $marcflavour = lc C4::Context->preference('marcflavour');
my @rules;
$self->_foreach_mapping(
sub {
my ( $name, $type, $facet, $suggestible, $sort, $marc_type, $marc_field ) = @_;
return if $marc_type ne $marcflavour;
my $options = '';
# There's a bug when using 'split' with something that
# selects a range
# The split makes everything into nested arrays, but that's not
# really a big deal, ES doesn't mind.
$options = '-split => 1' unless $marc_field =~ m|_/| || $type eq 'sum';
push @rules, "marc_map('$marc_field','${name}', $options)";
if ($facet) {
push @rules, "marc_map('$marc_field','${name}__facet', $options)";
}
if ($suggestible) {
push @rules,
"marc_map('$marc_field','${name}__suggestion.input.\$append', $options)";
}
if ( $type eq 'boolean' ) {
# boolean gets special handling, basically if it doesn't exist,
# it's added and set to false. Otherwise we can't query it.
push @rules,
"unless exists('$name') add_field('$name', 0) end";
}
if ($type eq 'sum' ) {
push @rules, "sum('$name')";
}
# Sort is a bit special as it can be true, false, undef. For
# fixer rules, we care about "true", or "undef" if there is
# special handling of this field from other one. "undef" means
# to do the default thing, which is make it sortable.
if ($self->sort_fields()->{$name}) {
if ($sort || !defined $sort) {
push @rules, "marc_map('$marc_field','${name}__sort', $options)";
}
}
}
);
return \@rules;
}
=head2 _foreach_mapping
$self->_foreach_mapping(
sub {
my ( $name, $type, $facet, $suggestible, $sort, $marc_type,
$marc_field )
= @_;
return unless $marc_type eq 'marc21';
print "Data comes from: " . $marc_field . "\n";
}
);
This allows you to apply a function to each entry in the elasticsearch mappings
table, in order to build the mappings for whatever is needed.
In the provided function, the files are:
=over 4
=item C<$name>
The field name for elasticsearch (corresponds to the 'mapping' column in the
database.
=item C<$type>
The type for this value, e.g. 'string'.
=item C<$facet>
True if this value should be facetised. This only really makes sense if the
field is understood by the facet processing code anyway.
=item C<$sort>
True if this is a field that a) needs special sort handling, and b) if it
should be sorted on. False if a) but not b). Undef if not a). This allows,
for example, author to be sorted on but not everything marked with "author"
to be included in that sort.
=item C<$marc_type>
A string that indicates the MARC type that this mapping is for, e.g. 'marc21',
'unimarc', 'normarc'.
=item C<$marc_field>
A string that describes the MARC field that contains the data to extract.
These are of a form suited to Catmandu's MARC fixers.
=back
=cut
sub _foreach_mapping {
my ( $self, $sub ) = @_;
# TODO use a caching framework here
my $search_fields = Koha::Database->schema->resultset('SearchField')->search(
{
'search_marc_map.index_name' => $self->index,
},
{ join => { search_marc_to_fields => 'search_marc_map' },
'+select' => [
'search_marc_to_fields.facet',
'search_marc_to_fields.suggestible',
'search_marc_to_fields.sort',
'search_marc_map.marc_type',
'search_marc_map.marc_field',
],
'+as' => [
'facet',
'suggestible',
'sort',
'marc_type',
'marc_field',
],
}
);
while ( my $search_field = $search_fields->next ) {
$sub->(
$search_field->name,
$search_field->type,
$search_field->get_column('facet'),
$search_field->get_column('suggestible'),
$search_field->get_column('sort'),
$search_field->get_column('marc_type'),
$search_field->get_column('marc_field'),
);
}
}
=head2 process_error
die process_error($@);
This parses an Elasticsearch error message and produces a human-readable
result from it. This result is probably missing all the useful information
that you might want in diagnosing an issue, so the warning is also logged.
Note that currently the resulting message is not internationalised. This
will happen eventually by some method or other.
=cut
sub process_error {
my ($self, $msg) = @_;
warn $msg; # simple logging
# This is super-primitive
return "Unable to understand your search query, please rephrase and try again.\n" if $msg =~ /ParseException/;
return "Unable to perform your search. Please try again.\n";
}
1;
__END__
=head1 AUTHOR
=over 4
=item Chris Cormack C<< <chrisc@catalyst.net.nz> >>
=item Robin Sheat C<< <robin@catalyst.net.nz> >>
=back
=cut

View File

@ -0,0 +1,207 @@
package Koha::ElasticSearch::Indexer;
# Copyright 2013 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Carp;
use Modern::Perl;
use base qw(Koha::ElasticSearch);
use Data::Dumper;
# For now just marc, but we can do anything here really
use Catmandu::Importer::MARC;
use Catmandu::Store::ElasticSearch;
Koha::ElasticSearch::Indexer->mk_accessors(qw( store ));
=head1 NAME
Koha::ElasticSearch::Indexer - handles adding new records to the index
=head1 SYNOPSIS
my $indexer = Koha::ElasticSearch::Indexer->new(
{ index => Koha::SearchEngine::BIBLIOS_INDEX } );
$indexer->drop_index();
$indexer->update_index(\@biblionumbers, \@records);
=head1 FUNCTIONS
=head2 $indexer->update_index($biblionums, $records);
C<$biblionums> is an arrayref containing the biblionumbers for the records.
C<$records> is an arrayref containing the L<MARC::Record>s themselves.
The values in the arrays must match up, and the 999$c value in the MARC record
will be rewritten using the values in C<$biblionums> to ensure they are correct.
If C<$biblionums> is C<undef>, this won't happen, but you should be sure that
999$c is correct on your own then.
Note that this will modify the original record if C<$biblionums> is supplied.
If that's a problem, clone them first.
=cut
sub update_index {
my ($self, $biblionums, $records) = @_;
# TODO should have a separate path for dealing with a large number
# of records at once where we use the bulk update functions in ES.
if ($biblionums) {
$self->_sanitise_records($biblionums, $records);
}
my $from = $self->_convert_marc_to_json($records);
if ( !$self->store ) {
my $params = $self->get_elasticsearch_params();
$self->store(
Catmandu::Store::ElasticSearch->new(
%$params,
index_settings => $self->get_elasticsearch_settings(),
index_mappings => $self->get_elasticsearch_mappings(),
)
);
}
$self->store->bag->add_many($from);
$self->store->bag->commit;
return 1;
}
=head2 $indexer->update_index_background($biblionums, $records)
This has exactly the same API as C<update_index_background> however it'll
return immediately. It'll start a background process that does the adding.
If it fails to add to Elasticsearch then it'll add to a queue that will cause
it to be updated by a regular index cron job in the future.
# TODO implement in the future - I don't know the best way of doing this yet.
# If fork: make sure process group is changed so apache doesn't wait for us.
=cut
sub update_index_background {
my $self = shift;
$self->update_index(@_);
}
=head2 $indexer->delete_index($biblionums)
C<$biblionums> is an arrayref of biblionumbers to delete from the index.
=cut
sub delete_index {
my ($self, $biblionums) = @_;
if ( !$self->store ) {
my $params = $self->get_elasticsearch_params();
$self->store(
Catmandu::Store::ElasticSearch->new(
%$params,
index_settings => $self->get_elasticsearch_settings(),
index_mappings => $self->get_elasticsearch_mappings(),
)
);
}
$self->store->bag->delete($_) foreach @$biblionums;
$self->store->bag->commit;
}
=head2 $indexer->delete_index_background($biblionums)
Identical to L<delete_index>, this will return immediately and start a
background process to do the actual deleting.
=cut
# TODO implement in the future
sub delete_index_background {
my $self = shift;
$self->delete_index(@_);
}
=head2 $indexer->drop_index();
Drops the index from the elasticsearch server. Calling C<update_index>
after this will recreate it again.
=cut
sub drop_index {
my ($self) = @_;
if (!$self->store) {
# If this index doesn't exist, this will create it. Then it'll be
# deleted. That's not the end of the world however.
my $params = $self->get_elasticsearch_params();
$self->store(
Catmandu::Store::ElasticSearch->new(
%$params,
index_settings => $self->get_elasticsearch_settings(),
index_mappings => $self->get_elasticsearch_mappings(),
)
);
}
$self->store->drop();
$self->store(undef);
}
sub _sanitise_records {
my ($self, $biblionums, $records) = @_;
confess "Unequal number of values in \$biblionums and \$records." if (@$biblionums != @$records);
my $c = @$biblionums;
for (my $i=0; $i<$c; $i++) {
my $bibnum = $biblionums->[$i];
my $rec = $records->[$i];
# I've seen things you people wouldn't believe. Attack ships on fire
# off the shoulder of Orion. I watched C-beams glitter in the dark near
# the Tannhauser gate. MARC records where 999$c doesn't match the
# biblionumber column. All those moments will be lost in time... like
# tears in rain...
$rec->delete_fields($rec->field('999'));
$rec->append_fields(MARC::Field->new('999','','','c' => $bibnum, 'd' => $bibnum));
}
}
sub _convert_marc_to_json {
my $self = shift;
my $records = shift;
my $importer =
Catmandu::Importer::MARC->new( records => $records, id => '999c' );
my $fixer = Catmandu::Fix->new( fixes => $self->get_fixer_rules() );
$importer = $fixer->fix($importer);
return $importer;
}
1;
__END__
=head1 AUTHOR
=over 4
=item Chris Cormack C<< <chrisc@catalyst.net.nz> >>
=item Robin Sheat C<< <robin@catalyst.net.nz> >>
=back

View File

@ -89,4 +89,8 @@ sub _type {
return 'Itemtype';
}
sub type {
return 'Itemtype';
}
1;

View File

@ -43,6 +43,10 @@ sub _type {
return 'Itemtype';
}
sub type {
return 'Itemtype';
}
sub object_class {
return 'Koha::ItemType';
}

74
Koha/MetadataIterator.pm Normal file
View File

@ -0,0 +1,74 @@
package Koha::MetadataIterator;
# This contains an iterator over biblio and authority records
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
Koha::MetadataIterator - iterates over records
=head1 DESCRIPTION
This provides a fairly generic iterator that will return records provided
by a function.
=head1 SYNOPSIS
use Koha::MetadataIterator;
my $next_func = sub {
# something that'll return each record
};
my $iterator = Koha::MetadataIterator->new($next_func);
while ( my $record = $iterator->next() ) {
# do something with $record
}
=head1 METHODS
=cut
use Modern::Perl;
=head2 new
my $it = new($next_func);
Takes a function that will provide the next bit of data.
=cut
sub new {
my ( $class, $next_func ) = @_;
bless { next_func => $next_func, }, $class;
}
=head2 next()
Provides the next record.
=cut
sub next {
my ($self) = @_;
return $self->{next_func}->();
}
1;

View File

@ -89,6 +89,24 @@ sub find {
return $object;
}
=head3 Koha::Objects->find_or_create();
my $object = Koha::Objects->find_or_create( $attrs );
=cut
sub find_or_create {
my ( $self, $params ) = @_;
my $result = $self->_resultset->find_or_create($params);
return unless $result;
my $object = $self->object_class->_new_from_dbic($result);
return $object;
}
=head3 Koha::Objects->search();
my @objects = Koha::Objects->search($params);

View File

@ -0,0 +1,122 @@
use utf8;
package Koha::Schema::Result::SearchField;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::SearchField
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<search_field>
=cut
__PACKAGE__->table("search_field");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 name
data_type: 'varchar'
is_nullable: 0
size: 255
the name of the field as it will be stored in the search engine
=head2 label
data_type: 'varchar'
is_nullable: 0
size: 255
the human readable name of the field, for display
=head2 type
data_type: 'enum'
extra: {list => ["string","date","number","boolean","sum"]}
is_nullable: 0
what type of data this holds, relevant when storing it in the search engine
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"name",
{ data_type => "varchar", is_nullable => 0, size => 255 },
"label",
{ data_type => "varchar", is_nullable => 0, size => 255 },
"type",
{
data_type => "enum",
extra => { list => ["string", "date", "number", "boolean", "sum"] },
is_nullable => 0,
},
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 UNIQUE CONSTRAINTS
=head2 C<name>
=over 4
=item * L</name>
=back
=cut
__PACKAGE__->add_unique_constraint("name", ["name"]);
=head1 RELATIONS
=head2 search_marc_to_fields
Type: has_many
Related object: L<Koha::Schema::Result::SearchMarcToField>
=cut
__PACKAGE__->has_many(
"search_marc_to_fields",
"Koha::Schema::Result::SearchMarcToField",
{ "foreign.search_field_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-10-12 16:41:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4MJO9216VF37w7PlWCcBKg
__PACKAGE__->many_to_many("search_marc_maps", "search_marc_to_fields", "search_marc_map");
1;

View File

@ -0,0 +1,130 @@
use utf8;
package Koha::Schema::Result::SearchMarcMap;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::SearchMarcMap
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<search_marc_map>
=cut
__PACKAGE__->table("search_marc_map");
=head1 ACCESSORS
=head2 id
data_type: 'integer'
is_auto_increment: 1
is_nullable: 0
=head2 index_name
data_type: 'enum'
extra: {list => ["biblios","authorities"]}
is_nullable: 0
what storage index this map is for
=head2 marc_type
data_type: 'enum'
extra: {list => ["marc21","unimarc","normarc"]}
is_nullable: 0
what MARC type this map is for
=head2 marc_field
data_type: 'varchar'
is_nullable: 0
size: 255
the MARC specifier for this field
=cut
__PACKAGE__->add_columns(
"id",
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
"index_name",
{
data_type => "enum",
extra => { list => ["biblios", "authorities"] },
is_nullable => 0,
},
"marc_type",
{
data_type => "enum",
extra => { list => ["marc21", "unimarc", "normarc"] },
is_nullable => 0,
},
"marc_field",
{ data_type => "varchar", is_nullable => 0, size => 255 },
);
=head1 PRIMARY KEY
=over 4
=item * L</id>
=back
=cut
__PACKAGE__->set_primary_key("id");
=head1 UNIQUE CONSTRAINTS
=head2 C<index_name>
=over 4
=item * L</index_name>
=item * L</marc_field>
=item * L</marc_type>
=back
=cut
__PACKAGE__->add_unique_constraint("index_name", ["index_name", "marc_field", "marc_type"]);
=head1 RELATIONS
=head2 search_marc_to_fields
Type: has_many
Related object: L<Koha::Schema::Result::SearchMarcToField>
=cut
__PACKAGE__->has_many(
"search_marc_to_fields",
"Koha::Schema::Result::SearchMarcToField",
{ "foreign.search_marc_map_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-10-12 16:41:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nKMOxnAJST3zNN6Kxj2ynA
__PACKAGE__->many_to_many("search_fields", "search_marc_to_fields", "search_field");
1;

View File

@ -0,0 +1,128 @@
use utf8;
package Koha::Schema::Result::SearchMarcToField;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Koha::Schema::Result::SearchMarcToField
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 TABLE: C<search_marc_to_field>
=cut
__PACKAGE__->table("search_marc_to_field");
=head1 ACCESSORS
=head2 search_marc_map_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=head2 search_field_id
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=head2 facet
data_type: 'tinyint'
default_value: 0
is_nullable: 1
true if a facet field should be generated for this
=head2 suggestible
data_type: 'tinyint'
default_value: 0
is_nullable: 1
true if this field can be used to generate suggestions for browse
=head2 sort
data_type: 'tinyint'
is_nullable: 1
true/false creates special sort handling, null doesn't
=cut
__PACKAGE__->add_columns(
"search_marc_map_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"search_field_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"facet",
{ data_type => "tinyint", default_value => 0, is_nullable => 1 },
"suggestible",
{ data_type => "tinyint", default_value => 0, is_nullable => 1 },
"sort",
{ data_type => "tinyint", is_nullable => 1 },
);
=head1 PRIMARY KEY
=over 4
=item * L</search_marc_map_id>
=item * L</search_field_id>
=back
=cut
__PACKAGE__->set_primary_key("search_marc_map_id", "search_field_id");
=head1 RELATIONS
=head2 search_field
Type: belongs_to
Related object: L<Koha::Schema::Result::SearchField>
=cut
__PACKAGE__->belongs_to(
"search_field",
"Koha::Schema::Result::SearchField",
{ id => "search_field_id" },
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 search_marc_map
Type: belongs_to
Related object: L<Koha::Schema::Result::SearchMarcMap>
=cut
__PACKAGE__->belongs_to(
"search_marc_map",
"Koha::Schema::Result::SearchMarcMap",
{ id => "search_marc_map_id" },
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
);
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-10-12 16:41:47
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ccnCYwDdtPGZ/VArAe1kPA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

46
Koha/SearchEngine.pm Normal file
View File

@ -0,0 +1,46 @@
package Koha::SearchEngine;
# This handles generic search-engine related functions
# Copyright 2015 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Readonly;
=head1 NAME
Koha::SearchEngine - non-engine-specific data and functions
=head1 VARIABLES / CONSTANTS
=head2 BIBLIOS_INDEX
Use this constant when creating a new L<Koha::SearchEngine::Search> instance
to indicate that you want to be working with the biblio index.
=head2 AUTHORITIES_INDEX
Use this constant when creating a new L<Koha::SearchEngine::Search> instance to
indicate that you want to be working with the authorities index.
=cut
# Search engine implementations should compare against these to determine
# what bit of storage is being requested. They will be sensible strings so
# may be used for, e.g., directory names.
Readonly our $BIBLIOS_INDEX => 'biblios';
Readonly our $AUTHORITIES_INDEX => 'authorities';

View File

@ -0,0 +1,771 @@
package Koha::SearchEngine::Elasticsearch::QueryBuilder;
# This file is part of Koha.
#
# Copyright 2014 Catalyst IT Ltd.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
=head1 NAME
Koha::SearchEngine::Elasticsearch::QueryBuilder - constructs elasticsearch
query objects from user-supplied queries
=head1 DESCRIPTION
This provides the functions that take a user-supplied search query, and
provides something that can be given to elasticsearch to get answers.
=head1 SYNOPSIS
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
$builder = Koha::SearchEngine::Elasticsearch->new({ index => $index });
my $simple_query = $builder->build_query("hello");
# This is currently undocumented because the original code is undocumented
my $adv_query = $builder->build_advanced_query($indexes, $operands, $operators);
=head1 METHODS
=cut
use base qw(Koha::ElasticSearch);
use Carp;
use JSON;
use List::MoreUtils qw/ each_array /;
use Modern::Perl;
use URI::Escape;
use C4::Context;
use Data::Dumper; # TODO remove
=head2 build_query
my $simple_query = $builder->build_query("hello", %options)
This will build a query that can be issued to elasticsearch from the provided
string input. This expects a lucene style search form (see
L<http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax>
for details.)
It'll make an attempt to respect the various query options.
Additional options can be provided with the C<%options> hash.
=over 4
=item sort
This should be an arrayref of hashrefs, each containing a C<field> and an
C<direction> (optional, defaults to C<asc>.) The results will be sorted
according to these values. Valid values for C<direction> are 'asc' and 'desc'.
=back
=cut
sub build_query {
my ( $self, $query, %options ) = @_;
my $stemming = C4::Context->preference("QueryStemming") || 0;
my $auto_truncation = C4::Context->preference("QueryAutoTruncate") || 0;
my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
$query = '*' unless defined $query;
my $res;
$res->{query} = {
query_string => {
query => $query,
fuzziness => $fuzzy_enabled ? 'auto' : '0',
default_operator => 'AND',
default_field => '_all',
lenient => JSON::true,
}
};
if ( $options{sort} ) {
foreach my $sort ( @{ $options{sort} } ) {
my ( $f, $d ) = @$sort{qw/ field direction /};
die "Invalid sort direction, $d"
if $d && ( $d ne 'asc' && $d ne 'desc' );
$d = 'asc' unless $d;
# TODO account for fields that don't have a 'phrase' type
$f = $self->_sort_field($f);
push @{ $res->{sort} }, { "$f.phrase" => { order => $d } };
}
}
# See _convert_facets in Search.pm for how these get turned into
# things that Koha can use.
$res->{facets} = {
author => { terms => { field => "author__facet" } },
subject => { terms => { field => "subject__facet" } },
itype => { terms => { field => "itype__facet" } },
location => { terms => { field => "homebranch__facet" } },
'su-geo' => { terms => { field => "su-geo__facet" } },
se => { terms => { field => "se__facet" } },
};
if ( my $ef = $options{expanded_facet} ) {
$res->{facets}{$ef}{terms}{size} = C4::Context->preference('FacetMaxCount');
};
return $res;
}
=head2 build_browse_query
my $browse_query = $builder->build_browse_query($field, $query);
This performs a "starts with" style query on a particular field. The field
to be searched must have been indexed with an appropriate mapping as a
"phrase" subfield, which pretty much everything has.
=cut
# XXX this isn't really a browse query like we want in the end
sub build_browse_query {
my ( $self, $field, $query ) = @_;
my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
return { query => '*' } if !defined $query;
# TODO this should come from Koha::Elasticsearch
my %field_whitelist = (
title => 1,
author => 1,
);
$field = 'title' if !exists $field_whitelist{$field};
my $sort = $self->_sort_field($field);
my $res = {
query => {
match_phrase_prefix => {
"$field.phrase" => {
query => $query,
operator => 'or',
fuzziness => $fuzzy_enabled ? 'auto' : '0',
}
}
},
sort => [ { "$sort.phrase" => { order => "asc" } } ],
};
}
=head2 build_query_compat
my (
$error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
$stopwords_removed, $query_type
)
= $builder->build_query_compat( \@operators, \@operands, \@indexes,
\@limits, \@sort_by, $scan, $lang );
This handles a search using the same api as L<C4::Search::buildQuery> does.
A very simple query will go in with C<$operands> set to ['query'], and
C<$sort_by> set to ['pubdate_dsc']. This simple case will return with
C<$query> set to something that can perform the search, C<$simple_query>
set to just the search term, C<$query_cgi> set to something that can
reproduce this search, and C<$query_desc> set to something else.
=cut
sub build_query_compat {
my ( $self, $operators, $operands, $indexes, $orig_limits, $sort_by, $scan,
$lang, $params )
= @_;
#die Dumper ( $self, $operators, $operands, $indexes, $orig_limits, $sort_by, $scan, $lang );
my @sort_params = $self->_convert_sort_fields(@$sort_by);
my @index_params = $self->_convert_index_fields(@$indexes);
my $limits = $self->_fix_limit_special_cases($orig_limits);
# Merge the indexes in with the search terms and the operands so that
# each search thing is a handy unit.
unshift @$operators, undef; # The first one can't have an op
my @search_params;
my $ea = each_array( @$operands, @$operators, @index_params );
while ( my ( $oand, $otor, $index ) = $ea->() ) {
next if ( !defined($oand) || $oand eq '' );
push @search_params, {
operand => $self->_clean_search_term($oand), # the search terms
operator => defined($otor) ? uc $otor : undef, # AND and so on
$index ? %$index : (),
};
}
# We build a string query from limits and the queries. An alternative
# would be to pass them separately into build_query and let it build
# them into a structured ES query itself. Maybe later, though that'd be
# more robust.
my $query_str = join( ' AND ',
join( ' ', $self->_create_query_string(@search_params) ) || (),
$self->_join_queries( $self->_convert_index_strings(@$limits) ) || () );
# If there's no query on the left, let's remove the junk left behind
$query_str =~ s/^ AND //;
my %options;
$options{sort} = \@sort_params;
$options{expanded_facet} = $params->{expanded_facet};
my $query = $self->build_query( $query_str, %options );
#die Dumper($query);
# We roughly emulate the CGI parameters of the zebra query builder
my $query_cgi;
$query_cgi = 'idx=kw&q=' . uri_escape_utf8( $operands->[0] ) if @$operands;
my $simple_query;
$simple_query = $operands->[0] if @$operands == 1;
my $query_desc = $simple_query;
my $limit = $self->_join_queries( $self->_convert_index_strings(@$limits));
my $limit_cgi = ( $orig_limits and @$orig_limits )
? '&limit=' . join( '&limit=', map { uri_escape_utf8($_) } @$orig_limits )
: '';
my $limit_desc;
$limit_desc = "$limit" if $limit;
return (
undef, $query, $simple_query, $query_cgi, $query_desc,
$limit, $limit_cgi, $limit_desc, undef, undef
);
}
=head2 build_authorities_query
my $query = $builder->build_authorities_query(\%search);
This takes a nice description of an authority search and turns it into a black-box
query that can then be passed to the appropriate searcher.
The search description is a hashref that looks something like:
{
searches => [
{
where => 'Heading', # search the main entry
operator => 'exact', # require an exact match
value => 'frogs', # the search string
},
{
where => '', # search all entries
operator => '', # default keyword, right truncation
value => 'pond',
},
],
sort => {
field => 'Heading',
order => 'desc',
},
authtypecode => 'TOPIC_TERM',
}
=cut
sub build_authorities_query {
my ( $self, $search ) = @_;
# Start by making the query parts
my @query_parts;
my @filter_parts;
foreach my $s ( @{ $search->{searches} } ) {
my ( $wh, $op, $val ) = @{$s}{qw(where operator value)};
$wh = '_all' if $wh eq '';
if ( $op eq 'is' || $op eq '=' ) {
# look for something that matches completely
# note, '=' is about numerical vals. May need special handling.
# _allphrase is a special field that only groups the exact
# matches. Also, we lowercase our search because the ES
# index lowercases its values, and term searches don't get the
# search analyzer applied to them.
push @filter_parts, { term => { "$wh.phrase" => lc $val } };
}
elsif ( $op eq 'exact' ) {
# left and right truncation, otherwise an exact phrase
push @query_parts, { match_phrase => { $wh => $val } };
}
elsif ( $op eq 'start' ) {
# startswith search
push @query_parts, { wildcard => { "$wh.phrase" => lc "$val*" } };
}
else {
# regular wordlist stuff
push @query_parts, { match => { $wh => $val } };
}
}
# Merge the query and filter parts appropriately
# 'should' behaves like 'or', if we want 'and', use 'must'
my $query_part = { bool => { should => \@query_parts } };
my $filter_part = { bool => { should => \@filter_parts } };
# We need to add '.phrase' to all the sort headings otherwise it'll sort
# based on the tokenised form.
my %s;
if ( exists $search->{sort} ) {
foreach my $k ( keys %{ $search->{sort} } ) {
my $f = $self->_sort_field($k);
$s{"$f.phrase"} = $search->{sort}{$k};
}
$search->{sort} = \%s;
}
# extract the sort stuff
my %sort;
%sort = ( sort => [ $search->{sort} ] ) if exists $search->{sort};
my $query;
if (@filter_parts) {
$query =
{ query =>
{ filtered => { filter => $filter_part, query => $query_part } }
};
}
else {
$query = { query => $query_part };
}
$query = { %$query, %sort };
return $query;
}
=head2 build_authorities_query_compat
my ($query) =
$builder->build_authorities_query_compat( \@marclist, \@and_or,
\@excluding, \@operator, \@value, $authtypecode, $orderby );
This builds a query for searching for authorities, in the style of
L<C4::AuthoritiesMarc::SearchAuthorities>.
Arguments:
=over 4
=item marclist
An arrayref containing where the particular term should be searched for.
Options are: mainmainentry, mainentry, match, match-heading, see-from, and
thesaurus. If left blank, any field is used.
=item and_or
Totally ignored. It is never used in L<C4::AuthoritiesMarc::SearchAuthorities>.
=item excluding
Also ignored.
=item operator
What form of search to do. Options are: is (phrase, no trunction, whole field
must match), = (number exact match), exact (phrase, but with left and right
truncation). If left blank, then word list, right truncted, anywhere is used.
=item value
The actual user-provided string value to search for.
=item authtypecode
The authority type code to search within. If blank, then all will be searched.
=item orderby
The order to sort the results by. Options are Relevance, HeadingAsc,
HeadingDsc, AuthidAsc, AuthidDsc.
=back
marclist, operator, and value must be the same length, and the values at
index /i/ all relate to each other.
This returns a query, which is a black box object that can be passed to the
appropriate search object.
=cut
sub build_authorities_query_compat {
my ( $self, $marclist, $and_or, $excluding, $operator, $value,
$authtypecode, $orderby )
= @_;
# This turns the old-style many-options argument form into a more
# extensible hash form that is understood by L<build_authorities_query>.
my @searches;
my %koha_to_index_name = (
mainmainentry => 'Heading-Main',
mainentry => 'Heading',
match => 'Match',
'match-heading' => 'Match-heading',
'see-from' => 'Match-heading-see-from',
thesaurus => 'Subject-heading-thesaurus',
any => '',
);
# Make sure everything exists
foreach my $m (@$marclist) {
confess "Invalid marclist field provided: $m" unless exists $koha_to_index_name{$m};
}
for ( my $i = 0 ; $i < @$value ; $i++ ) {
push @searches,
{
where => $koha_to_index_name{$marclist->[$i]},
operator => $operator->[$i],
value => $value->[$i],
};
}
my %sort;
my $sort_field =
( $orderby =~ /^Heading/ ) ? 'Heading'
: ( $orderby =~ /^Auth/ ) ? 'Local-Number'
: undef;
if ($sort_field) {
my $sort_order = ( $orderby =~ /Asc$/ ) ? 'asc' : 'desc';
%sort = ( $sort_field => $sort_order, );
}
my %search = (
searches => \@searches,
authtypecode => $authtypecode,
);
$search{sort} = \%sort if %sort;
my $query = $self->build_authorities_query( \%search );
return $query;
}
=head2 _convert_sort_fields
my @sort_params = _convert_sort_fields(@sort_by)
Converts the zebra-style sort index information into elasticsearch-style.
C<@sort_by> is the same as presented to L<build_query_compat>, and it returns
something that can be sent to L<build_query>.
=cut
sub _convert_sort_fields {
my ( $self, @sort_by ) = @_;
# Turn the sorting into something we care about.
my %sort_field_convert = (
acqdate => 'acqdate',
author => 'author',
call_number => 'callnum',
popularity => 'issues',
relevance => undef, # default
title => 'title',
pubdate => 'pubdate',
);
my %sort_order_convert =
( qw( dsc desc ), qw( asc asc ), qw( az asc ), qw( za desc ) );
# Convert the fields and orders, drop anything we don't know about.
grep { $_->{field} } map {
my ( $f, $d ) = split /_/;
{
field => $sort_field_convert{$f},
direction => $sort_order_convert{$d}
}
} @sort_by;
}
=head2 _convert_index_fields
my @index_params = $self->_convert_index_fields(@indexes);
Converts zebra-style search index notation into elasticsearch-style.
C<@indexes> is an array of index names, as presented to L<build_query_compat>,
and it returns something that can be sent to L<build_query>.
B<TODO>: this will pull from the elasticsearch mappings table to figure out
types.
=cut
our %index_field_convert = (
'kw' => '_all',
'ti' => 'title',
'au' => 'author',
'su' => 'subject',
'nb' => 'isbn',
'se' => 'title-series',
'callnum' => 'callnum',
'itype' => 'itype',
'ln' => 'ln',
'branch' => 'homebranch',
'fic' => 'lf',
'mus' => 'rtype',
'aud' => 'ta',
'hi' => 'Host-Item-Number',
);
sub _convert_index_fields {
my ( $self, @indexes ) = @_;
my %index_type_convert =
( __default => undef, phr => 'phrase', rtrn => 'right-truncate' );
# Convert according to our table, drop anything that doesn't convert.
# If a field starts with mc- we save it as it's used (and removed) later
# when joining things, to indicate we make it an 'OR' join.
# (Sorry, this got a bit ugly after special cases were found.)
grep { $_->{field} } map {
my ( $f, $t ) = split /,/;
my $mc = '';
if ($f =~ /^mc-/) {
$mc = 'mc-';
$f =~ s/^mc-//;
}
my $r = {
field => $index_field_convert{$f},
type => $index_type_convert{ $t // '__default' }
};
$r->{field} = ($mc . $r->{field}) if $mc && $r->{field};
$r;
} @indexes;
}
=head2 _convert_index_strings
my @searches = $self->_convert_index_strings(@searches);
Similar to L<_convert_index_fields>, this takes strings of the form
B<field:search term> and rewrites the field from zebra-style to
elasticsearch-style. Anything it doesn't understand is returned verbatim.
=cut
sub _convert_index_strings {
my ( $self, @searches ) = @_;
my @res;
foreach my $s (@searches) {
next if $s eq '';
my ( $field, $term ) = $s =~ /^\s*([\w,-]*?):(.*)/;
unless ( defined($field) && defined($term) ) {
push @res, $s;
next;
}
my ($conv) = $self->_convert_index_fields($field);
unless ( defined($conv) ) {
push @res, $s;
next;
}
push @res, $conv->{field} . ":"
. $self->_modify_string_by_type( %$conv, operand => $term );
}
return @res;
}
=head2 _convert_index_strings_freeform
my $search = $self->_convert_index_strings_freeform($search);
This is similar to L<_convert_index_strings>, however it'll search out the
things to change within the string. So it can handle strings such as
C<(su:foo) AND (su:bar)>, converting the C<su> appropriately.
If there is something of the form "su,complete-subfield" or something, the
second part is stripped off as we can't yet handle that. Making it work
will have to wait for a real query parser.
=cut
sub _convert_index_strings_freeform {
my ( $self, $search ) = @_;
while ( my ( $zeb, $es ) = each %index_field_convert ) {
$search =~ s/\b$zeb(?:,[\w-]*)?:/$es:/g;
}
return $search;
}
=head2 _modify_string_by_type
my $str = $self->_modify_string_by_type(%index_field);
If you have a search term (operand) and a type (phrase, right-truncated), this
will convert the string to have the function in lucene search terms, e.g.
wrapping quotes around it.
=cut
sub _modify_string_by_type {
my ( $self, %idx ) = @_;
my $type = $idx{type} || '';
my $str = $idx{operand};
return $str unless $str; # Empty or undef, we can't use it.
$str .= '*' if $type eq 'right-truncate';
$str = '"' . $str . '"' if $type eq 'phrase';
return $str;
}
=head2 _join_queries
my $query_str = $self->_join_queries(@query_parts);
This takes a list of query parts, that might be search terms on their own, or
booleaned together, or specifying fields, or whatever, wraps them in
parentheses, and ANDs them all together. Suitable for feeding to the ES
query string query.
Note: doesn't AND them together if they specify an index that starts with "mc"
as that was a special case in the original code for dealing with multiple
choice options (you can't search for something that has an itype of A and
and itype of B otherwise.)
=cut
sub _join_queries {
my ( $self, @parts ) = @_;
my @norm_parts = grep { defined($_) && $_ ne '' && $_ !~ /^mc-/ } @parts;
my @mc_parts =
map { s/^mc-//r } grep { defined($_) && $_ ne '' && $_ =~ /^mc-/ } @parts;
return () unless @norm_parts + @mc_parts;
return ( @norm_parts, @mc_parts )[0] if @norm_parts + @mc_parts == 1;
my $grouped_mc =
@mc_parts ? '(' . ( join ' OR ', map { "($_)" } @mc_parts ) . ')' : ();
# Handy trick: $x || () inside a join means that if $x ends up as an
# empty string, it gets replaced with (), which makes join ignore it.
# (bad effect: this'll also happen to '0', this hopefully doesn't matter
# in this case.)
join( ' AND ',
join( ' AND ', map { "($_)" } @norm_parts ) || (),
$grouped_mc || () );
}
=head2 _make_phrases
my @phrased_queries = $self->_make_phrases(@query_parts);
This takes the supplied queries and forces them to be phrases by wrapping
quotes around them. It understands field prefixes, e.g. 'subject:' and puts
the quotes outside of them if they're there.
=cut
sub _make_phrases {
my ( $self, @parts ) = @_;
map { s/^\s*(\w*?:)(.*)$/$1"$2"/r } @parts;
}
=head2 _create_query_string
my @query_strings = $self->_create_query_string(@queries);
Given a list of hashrefs, it will turn them into a lucene-style query string.
The hash should contain field, type (both for the indexes), operator, and
operand.
=cut
sub _create_query_string {
my ( $self, @queries ) = @_;
map {
my $otor = $_->{operator} ? $_->{operator} . ' ' : '';
my $field = $_->{field} ? $_->{field} . ':' : '';
my $oand = $self->_modify_string_by_type(%$_);
"$otor($field$oand)";
} @queries;
}
=head2 _clean_search_term
my $term = $self->_clean_search_term($term);
This cleans a search term by removing any funny characters that may upset
ES and give us an error. It also calls L<_convert_index_strings_freeform>
to ensure those parts are correct.
=cut
sub _clean_search_term {
my ( $self, $term ) = @_;
# Some hardcoded searches (like with authorities) produce things like
# 'an=123', when it ought to be 'an:123' for our purposes.
$term =~ s/=/:/g;
$term = $self->_convert_index_strings_freeform($term);
$term =~ s/[{}]/"/g;
return $term;
}
=head2 _fix_limit_special_cases
my $limits = $self->_fix_limit_special_cases($limits);
This converts any special cases that the limit specifications have into things
that are more readily processable by the rest of the code.
The argument should be an arrayref, and it'll return an arrayref.
=cut
sub _fix_limit_special_cases {
my ( $self, $limits ) = @_;
my @new_lim;
foreach my $l (@$limits) {
# This is set up by opac-search.pl
if ( $l =~ /^yr,st-numeric,ge=/ ) {
my ( $start, $end ) =
( $l =~ /^yr,st-numeric,ge=(.*) and yr,st-numeric,le=(.*)$/ );
next unless defined($start) && defined($end);
push @new_lim, "copydate:[$start TO $end]";
}
elsif ( $l =~ /^yr,st-numeric=/ ) {
my ($date) = ( $l =~ /^yr,st-numeric=(.*)$/ );
next unless defined($date);
push @new_lim, "copydate:$date";
}
elsif ( $l =~ /^available$/ ) {
push @new_lim, 'onloan:false';
}
else {
push @new_lim, $l;
}
}
return \@new_lim;
}
=head2 _sort_field
my $field = $self->_sort_field($field);
Given a field name, this works out what the actual name of the version to sort
on should be. Often it's the same, sometimes it involves sticking "__sort" on
the end. Maybe it'll be something else in the future, who knows?
=cut
sub _sort_field {
my ($self, $f) = @_;
if ($self->sort_fields()->{$f}) {
$f .= '__sort';
}
return $f;
}
1;

View File

@ -0,0 +1,458 @@
package Koha::SearchEngine::Elasticsearch::Search;
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
Koha::SearchEngine::ElasticSearch::Search - search functions for Elasticsearch
=head1 SYNOPSIS
my $searcher =
Koha::SearchEngine::ElasticSearch::Search->new( { index => $index } );
my $builder = Koha::SearchEngine::Elasticsearch::QueryBuilder->new(
{ index => $index } );
my $query = $builder->build_query('perl');
my $results = $searcher->search($query);
print "There were " . $results->total . " results.\n";
$results->each(sub {
push @hits, @_[0];
});
=head1 METHODS
=cut
use Modern::Perl;
use base qw(Koha::ElasticSearch);
use C4::Context;
use Koha::ItemTypes;
use Koha::AuthorisedValues;
use Koha::SearchEngine::QueryBuilder;
use MARC::Record;
use Catmandu::Store::ElasticSearch;
use Data::Dumper; #TODO remove
use Carp qw(cluck);
Koha::SearchEngine::Elasticsearch::Search->mk_accessors(qw( store ));
=head2 search
my $results = $searcher->search($query, $page, $count, %options);
Run a search using the query. It'll return C<$count> results, starting at page
C<$page> (C<$page> counts from 1, anything less that, or C<undef> becomes 1.)
C<$count> is also the number of entries on a page.
C<%options> is a hash containing extra options:
=over 4
=item offset
If provided, this overrides the C<$page> value, and specifies the record as
an offset (i.e. the number of the record to start with), rather than a page.
=back
Returns
=cut
sub search {
my ($self, $query, $page, $count, %options) = @_;
my $params = $self->get_elasticsearch_params();
my %paging;
# 20 is the default number of results per page
$paging{limit} = $count || 20;
# ES/Catmandu doesn't want pages, it wants a record to start from.
if (exists $options{offset}) {
$paging{start} = $options{offset};
} else {
$page = (!defined($page) || ($page <= 0)) ? 0 : $page - 1;
$paging{start} = $page * $paging{limit};
}
$self->store(
Catmandu::Store::ElasticSearch->new(
%$params,
)
) unless $self->store;
my $results = eval {
$self->store->bag->search( %$query, %paging );
};
if ($@) {
die $self->process_error($@);
}
return $results;
}
=head2 count
my $count = $searcher->count($query);
This mimics a search request, but just gets the result count instead. That's
faster than pulling all the data in, usually.
=cut
sub count {
my ( $self, $query ) = @_;
my $params = $self->get_elasticsearch_params();
$self->store(
Catmandu::Store::ElasticSearch->new( %$params, trace_calls => 0, ) )
unless $self->store;
my $search = $self->store->bag->search( %$query);
my $count = $search->total() || 0;
return $count;
}
=head2 search_compat
my ( $error, $results, $facets ) = $search->search_compat(
$query, $simple_query, \@sort_by, \@servers,
$results_per_page, $offset, $expanded_facet, $branches,
$query_type, $scan
)
A search interface somewhat compatible with L<C4::Search->getRecords>. Anything
that is returned in the query created by build_query_compat will probably
get ignored here, along with some other things (like C<@servers>.)
=cut
sub search_compat {
my (
$self, $query, $simple_query, $sort_by,
$servers, $results_per_page, $offset, $expanded_facet,
$branches, $query_type, $scan
) = @_;
my %options;
$options{offset} = $offset;
$options{expanded_facet} = $expanded_facet;
my $results = $self->search($query, undef, $results_per_page, %options);
# Convert each result into a MARC::Record
my (@records, $index);
$index = $offset; # opac-search expects results to be put in the
# right place in the array, according to $offset
$results->each(sub {
# The results come in an array for some reason
my $marc_json = $_[0]->{record};
my $marc = $self->json2marc($marc_json);
$records[$index++] = $marc;
});
# consumers of this expect a name-spaced result, we provide the default
# configuration.
my %result;
$result{biblioserver}{hits} = $results->total;
$result{biblioserver}{RECORDS} = \@records;
return (undef, \%result, $self->_convert_facets($results->{facets}, $expanded_facet));
}
=head2 search_auth_compat
my ( $results, $total ) =
$searcher->search_auth_compat( $query, $page, $count, %options );
This has a similar calling convention to L<search>, however it returns its
results in a form the same as L<C4::AuthoritiesMarc::SearchAuthorities>.
=cut
sub search_auth_compat {
my $self = shift;
# TODO handle paging
my $database = Koha::Database->new();
my $schema = $database->schema();
my $res = $self->search(@_);
my $bib_searcher = Koha::SearchEngine::Elasticsearch::Search->new({index => 'biblios'});
my @records;
$res->each(
sub {
my %result;
my $record = $_[0];
my $marc_json = $record->{record};
# I wonder if these should be real values defined in the mapping
# rather than hard-coded conversions.
# Our results often come through as nested arrays, to fix this
# requires changes in catmandu.
my $authid = $record->{ 'Local-number' }[0][0];
$result{authid} = $authid;
# TODO put all this info into the record at index time so we
# don't have to go and sort it all out now.
my $authtypecode = $record->{authtype};
my $rs = $schema->resultset('AuthType')
->search( { authtypecode => $authtypecode } );
# FIXME there's an assumption here that we will get a result.
# the original code also makes an assumption that some provided
# authtypecode may sometimes be used instead of the one stored
# with the record. It's not documented why this is the case, so
# it's not reproduced here yet.
my $authtype = $rs->single;
my $auth_tag_to_report = $authtype->auth_tag_to_report;
my $marc = $self->json2marc($marc_json);
my $mainentry = $marc->field($auth_tag_to_report);
my $reported_tag;
if ($mainentry) {
foreach ( $mainentry->subfields() ) {
$reported_tag .= '$' . $_->[0] . $_->[1];
}
}
# Turn the resultset into a hash
my %authtype_cols;
foreach my $col ($authtype->result_source->columns) {
$authtype_cols{$col} = $authtype->get_column($col);
}
$result{authtype} = $authtype->authtypetext;
$result{reported_tag} = $reported_tag;
# Reimplementing BuildSummary is out of scope because it'll be hard
$result{summary} =
C4::AuthoritiesMarc::BuildSummary( $marc, $result{authid},
$authtypecode );
$result{used} = $self->count_auth_use($bib_searcher, $authid);
push @records, \%result;
}
);
return ( \@records, $res->total );
}
=head2 count_auth_use
my $count = $auth_searcher->count_auth_use($bib_searcher, $authid);
This runs a search to determine the number of records that reference the
specified authid. C<$bib_searcher> must be something compatible with
elasticsearch, as the query is built in this function.
=cut
sub count_auth_use {
my ($self, $bib_searcher, $authid) = @_;
my $query = {
query => {
filtered => {
query => { match_all => {} },
filter => { term => { an => $authid } }
}
}
};
$bib_searcher->count($query);
}
=head2 simple_search_compat
my ( $error, $marcresults, $total_hits ) =
$searcher->simple_search( $query, $offset, $max_results );
This is a simpler interface to the searching, intended to be similar enough to
L<C4::Search::SimpleSearch>.
Arguments:
=over 4
=item C<$query>
A thing to search for. It could be a simple string, or something constructed
with the appropriate QueryBuilder module.
=item C<$offset>
How many results to skip from the start of the results.
=item C<$max_results>
The max number of results to return. The default is 100 (because unlimited
is a pretty terrible thing to do.)
=back
Returns:
=over 4
=item C<$error>
if something went wrong, this'll contain some kind of error
message.
=item C<$marcresults>
an arrayref of MARC::Records (note that this is different from the
L<C4::Search> version which will return plain XML, but too bad.)
=item C<$total_hits>
the total number of results that this search could have returned.
=back
=cut
sub simple_search_compat {
my ($self, $query, $offset, $max_results) = @_;
return ('No query entered', undef, undef) unless $query;
my %options;
$options{offset} = $offset // 0;
$max_results //= 100;
unless (ref $query) {
# We'll push it through the query builder to sanitise everything.
my $qb = Koha::SearchEngine::QueryBuilder->new({index => $self->index});
(undef,$query) = $qb->build_query_compat(undef, [$query]);
}
my $results = $self->search($query, undef, $max_results, %options);
my @records;
$results->each(sub {
# The results come in an array for some reason
my $marc_json = $_[0]->{record};
my $marc = $self->json2marc($marc_json);
push @records, $marc;
});
return (undef, \@records, $results->total);
}
=head2 json2marc
my $marc = $self->json2marc($marc_json);
Converts the form of marc (based on its JSON, but as a Perl structure) that
Catmandu stores into a MARC::Record object.
=cut
sub json2marc {
my ( $self, $marcjson ) = @_;
my $marc = MARC::Record->new();
$marc->encoding('UTF-8');
# fields are like:
# [ '245', '1', '2', 'a' => 'Title', 'b' => 'Subtitle' ]
# conveniently, this is the form that MARC::Field->new() likes
foreach my $field (@$marcjson) {
next if @$field < 5; # Shouldn't be possible, but...
if ( $field->[0] eq 'LDR' ) {
$marc->leader( $field->[4] );
}
else {
my $marc_field = MARC::Field->new(@$field);
$marc->append_fields($marc_field);
}
}
return $marc;
}
=head2 _convert_facets
my $koha_facets = _convert_facets($es_facets, $expanded_facet);
Converts elasticsearch facets types to the form that Koha expects.
It expects the ES facet name to match the Koha type, for example C<itype>,
C<au>, C<su-to>, etc.
C<$expanded_facet> is the facet that we want to show FacetMaxCount entries for, rather
than just 5 like normal.
=cut
sub _convert_facets {
my ( $self, $es, $exp_facet ) = @_;
return if !$es;
# These should correspond to the ES field names, as opposed to the CCL
# things that zebra uses.
# TODO let the library define the order using the interface.
my %type_to_label = (
author => { order => 1, label => 'Authors', },
itype => { order => 2, label => 'ItemTypes', },
location => { order => 3, label => 'Location', },
'su-geo' => { order => 4, label => 'Places', },
se => { order => 5, label => 'Series', },
subject => { order => 6, label => 'Topics', },
);
# We also have some special cases, e.g. itypes that need to show the
# value rather than the code.
my @itypes = Koha::ItemTypes->search;
my @locations = Koha::AuthorisedValues->search( { category => 'LOC' } );
my $opac = C4::Context->interface eq 'opac' ;
my %special = (
itype => { map { $_->itemtype => $_->description } @itypes },
location => { map { $_->authorised_value => ( $opac ? ( $_->lib_opac || $_->lib ) : $_->lib ) } @locations },
);
my @facets;
$exp_facet //= '';
while ( my ( $type, $data ) = each %$es ) {
next if !exists( $type_to_label{$type} );
# We restrict to the most popular $limit !results
my $limit = ( $type eq $exp_facet ) ? C4::Context->preference('FacetMaxCount') : 5;
my $facet = {
type_id => $type . '_id',
expand => $type,
expandable => ( $type ne $exp_facet )
&& ( @{ $data->{terms} } > $limit ),
"type_label_$type_to_label{$type}{label}" => 1,
type_link_value => $type,
order => $type_to_label{$type}{order},
};
$limit = @{ $data->{terms} } if ( $limit > @{ $data->{terms} } );
foreach my $term ( @{ $data->{terms} }[ 0 .. $limit - 1 ] ) {
my $t = $term->{term};
my $c = $term->{count};
my $label;
if ( exists( $special{$type} ) ) {
$label = $special{$type}->{$t} // $t;
}
else {
$label = $t;
}
push @{ $facet->{facets} }, {
facet_count => $c,
facet_link_value => $t,
facet_title_value => $t . " ($c)",
facet_label_value => $label, # TODO either truncate this,
# or make the template do it like it should anyway
type_link_value => $type,
};
}
push @facets, $facet if exists $facet->{facets};
}
@facets = sort { $a->{order} cmp $b->{order} } @facets;
return \@facets;
}
1;

View File

@ -0,0 +1,60 @@
package Koha::SearchEngine::QueryBuilder;
# This file is part of Koha.
#
# Copyright 2014 Catalyst IT
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
# This is a shim that gives you the appropriate query builder for your
# system preference.
=head1 NAME
Koha::SearchEngine::QueryBuilder - instantiates the query builder that
corresponds to the C<SearchEngine> system preference
=head1 DESCRIPTION
This allows you to be agnostic about what the search engine configuration is
and just get whatever querybuilder you need.
=head1 SYNOPSIS
use Koha::SearchEngine::QueryBuilder;
my $qb = Koha::SearchEngine::QueryBuilder->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
=head1 METHODS
=head2 new
Creates a new C<QueryBuilder> of whatever the relevant type is.
=cut
use C4::Context;
use Modern::Perl;
use Carp;
sub new {
my $engine = C4::Context->preference("SearchEngine");
my $file = "Koha/SearchEngine/${engine}/QueryBuilder.pm";
my $class = "Koha::SearchEngine::${engine}::QueryBuilder";
croak "SearchEngine system preference not set" unless $engine;
require $file;
shift @_;
return $class->new(@_);
}
1;

View File

@ -0,0 +1,58 @@
package Koha::SearchEngine::Search;
# This file is part of Koha.
#
# Copyright 2015 Catalyst IT
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
# This is a shim that gives you the appropriate search object for your
# system preference.
=head1 NAME
Koha::SearchEngine::Search - instantiate the search object that corresponds to
the C<SearchEngine> system preference.
=head1 DESCRIPTION
This allows you to be agnostic about what the search engine configuration is
and just get whatever search object you need.
=head1 SYNOPSIS
use Koha::SearchEngine::Search;
my $searcher = Koha::SearchEngine::Search->new();
=head1 METHODS
=head2 new
Creates a new C<Search> of whatever the relevant type is.
=cut
use C4::Context;
use Modern::Perl;
sub new {
my $engine = C4::Context->preference("SearchEngine") // 'Zebra';
my $file = "Koha/SearchEngine/${engine}/Search.pm";
my $class = "Koha::SearchEngine::${engine}::Search";
require $file;
shift @_;
return $class->new(@_);
}
1;

View File

@ -0,0 +1,56 @@
package Koha::SearchEngine::Zebra::QueryBuilder;
# This file is part of Koha.
#
# Copyright 2012 BibLibre
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use base qw(Class::Accessor);
use Modern::Perl;
use Moose::Role;
use C4::Search;
use C4::AuthoritiesMarc;
sub build_query {
shift;
C4::Search::buildQuery @_;
}
sub build_query_compat {
# Because this passes directly on to C4::Search, we have no trouble being
# compatible.
build_query(@_);
}
sub build_authorities_query {
shift;
C4::AuthoritiesMarc::SearchAuthorities(@_);
return {
marclist => $_[0],
and_or => $_[1],
excluding => $_[2],
operator => $_[3],
value => $_[4],
authtypecode => $_[5],
orderby => $_[6],
};
}
sub build_authorities_query_compat {
# Pass straight through as well
build_authorities_query(@_);
}
1;

View File

@ -0,0 +1,100 @@
package Koha::SearchEngine::Zebra::Search;
# This file is part of Koha.
#
# Copyright 2012 BibLibre
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
# I don't think this ever worked right
#use Moose::Role;
#with 'Koha::SearchEngine::SearchRole';
use Modern::Perl;
use base qw(Class::Accessor);
use C4::Search; # :(
use C4::AuthoritiesMarc;
=head1 NAME
Koha::SearchEngine::Zebra::Search - Search implementation for Zebra
=head1 METHODS
=head2 search
=cut
sub search {
my ($self,$query_string) = @_;
my $query = Data::SearchEngine::Query->new(
count => 10,
page => 1,
query => $query_string,
);
my $results = $self->searchengine->search($query);
foreach my $item (@{ $results->items }) {
my $title = $item->get_value('ste_title');
#utf8::encode($title);
print "$title\n";
}
}
=head2 search_compat
This passes straight through to C4::Search::getRecords.
=cut
sub search_compat {
shift; # get rid of $self
return getRecords(@_);
}
=head2 simple_search_compat
This passes straight through to C4::Search::SimpleSearch.
=cut
sub simple_search_compat {
shift;
return C4::Search::SimpleSearch(@_);
}
=head2 search_auth_compat
This passes the search query on to C4::AuthoritiesMarc::SearchAuthorities
=cut
sub search_auth_compat {
my ( $self, $q, $startfrom, $resperpage ) = @_;
my @params = (
@{$q}{ 'marclist', 'and_or', 'excluding', 'operator', 'value' },
$startfrom - 1,
$resperpage, @{$q}{ 'authtypecode', 'orderby' }
);
C4::AuthoritiesMarc::SearchAuthorities(@params);
}
1;

49
Koha/SearchField.pm Normal file
View File

@ -0,0 +1,49 @@
package Koha::SearchField;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Carp;
use Koha::Database;
use base qw(Koha::Object);
=head1 NAME
Koha::SearchField - Koha SearchField Object class
=head1 API
=head2 Class Methods
=cut
sub add_to_search_marc_maps {
my ( $self, $search_field, $params ) = @_;
return $self->_result()->add_to_search_marc_maps($search_field->_result, $params);
}
=head3 type
=cut
sub _type {
return 'SearchField';
}
1;

50
Koha/SearchFields.pm Normal file
View File

@ -0,0 +1,50 @@
package Koha::SearchFields;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Carp;
use Koha::Database;
use Koha::SearchField;
use base qw(Koha::Objects);
=head1 NAME
Koha::SearchFields - Koha SearchField Object set class
=head1 API
=head2 Class Methods
=cut
=head3 type
=cut
sub _type {
return 'SearchField';
}
sub object_class {
return 'Koha::SearchField';
}
1;

50
Koha/SearchMarcMap.pm Normal file
View File

@ -0,0 +1,50 @@
package Koha::SearchMarcMap;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Carp;
use Koha::Database;
use base qw(Koha::Object);
=head1 NAME
Koha::SearchMarcMap - Koha SearchMarcMap Object class
=head1 API
=head2 Class Methods
=cut
sub add_to_search_fields {
my( $self, $params) = @_;
$self->_result->add_to_search_fields($params);
}
=head3 type
=cut
sub _type {
return 'SearchMarcMap';
}
1;

50
Koha/SearchMarcMaps.pm Normal file
View File

@ -0,0 +1,50 @@
package Koha::SearchMarcMaps;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Carp;
use Koha::Database;
use Koha::SearchMarcMap;
use base qw(Koha::Objects);
=head1 NAME
Koha::SearchMarcMaps - Koha SearchMarcMap Object set class
=head1 API
=head2 Class Methods
=cut
=head3 type
=cut
sub _type {
return 'SearchMarcMap';
}
sub object_class {
return 'Koha::SearchMarcMap';
}
1;

View File

@ -68,6 +68,9 @@ use C4::Members qw/ GetMember /;
use C4::Budgets qw/ GetBudgetHierarchy /;
use Koha::Acquisition::Bookseller;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
my $input = new CGI;
@ -99,13 +102,14 @@ my @operands = $query;
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $builtquery;
my $builder = Koha::SearchEngine::QueryBuilder->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
if ($QParser) {
$builtquery = $query;
} else {
my ( $builterror,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
( $builterror,$builtquery,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type) = buildQuery(undef,\@operands);
( undef,$builtquery,undef,undef,undef,undef,undef,undef,undef,undef) = $builder->build_query_compat(undef,\@operands);
}
my ( $error, $marcresults, $total_hits ) = SimpleSearch( $builtquery, $results_per_page * ( $page - 1 ), $results_per_page );
my ( $error, $marcresults, $total_hits ) = $searcher->simple_search_compat($builtquery, $results_per_page * ($page - 1), $results_per_page);
if (defined $error) {
$template->param(

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,8 @@ use C4::Biblio;
use C4::Search::History;
use Koha::Authority::Types;
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
my $query = new CGI;
my $dbh = C4::Context->dbh;
@ -77,13 +79,25 @@ if ( $op eq "do_search" ) {
my $startfrom = $query->param('startfrom') || 1;
my $resultsperpage = $query->param('resultsperpage') || 20;
my ( $results, $total ) = SearchAuthorities(
[$marclist], [$and_or],
[$excluding], [$operator],
[$value], ( $startfrom - 1 ) * $resultsperpage,
$resultsperpage, $authtypecode,
$orderby
my $builder = Koha::SearchEngine::QueryBuilder->new(
{ index => $Koha::SearchEngine::AUTHORITIES_INDEX } );
my $searcher = Koha::SearchEngine::Search->new(
{ index => $Koha::SearchEngine::AUTHORITIES_INDEX } );
my $search_query = $builder->build_authorities_query_compat(
[$marclist], [$and_or], [$excluding], [$operator],
[$value], $authtypecode, $orderby
);
$startfrom = $startfrom // 0;
my ( $results, $total ) =
$searcher->search_auth_compat( $search_query, $startfrom,
$resultsperpage );
#my ( $results, $total ) = SearchAuthorities(
# [$marclist], [$and_or],
# [$excluding], [$operator],
# [$value], ( $startfrom - 1 ) * $resultsperpage,
# $resultsperpage, $authtypecode,
# $orderby
#);
( $template, $loggedinuser, $cookie ) = get_template_and_user(

View File

@ -5,6 +5,8 @@ use C4::Context;
use C4::AuthoritiesMarc;
use utf8;
use open qw[ :std :encoding(utf8) ];
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
my $dbh=C4::Context->dbh;
my $datatypes_query = $dbh->prepare(<<ENDSQL);
@ -24,7 +26,8 @@ foreach my $authority (@$dataauthorities){
my $query;
$query= "an=".$authority->{'authid'};
# search for biblios mapped
my ($err,$res,$used) = C4::Search::SimpleSearch($query,0,10);
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my ($err,undef,$used) = $searcher->simple_search_compat($query,0,1);
if (defined $err) {
$used = 0;
}

View File

@ -154,6 +154,8 @@ use C4::Search::History;
use Koha::LibraryCategories;
use Koha::Virtualshelves;
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
use URI::Escape;
@ -203,30 +205,6 @@ if($cgi->cookie("holdfor")){
);
}
## URI Re-Writing
# Deprecated, but preserved because it's interesting :-)
# The same thing can be accomplished with mod_rewrite in
# a more elegant way
#
#my $rewrite_flag;
#my $uri = $cgi->url(-base => 1);
#my $relative_url = $cgi->url(-relative=>1);
#$uri.="/".$relative_url."?";
#warn "URI:$uri";
#my @cgi_params_list = $cgi->param();
#my $url_params = $cgi->Vars;
#
#for my $each_param_set (@cgi_params_list) {
# $uri.= join "", map "\&$each_param_set=".$_, split("\0",$url_params->{$each_param_set}) if $url_params->{$each_param_set};
#}
#warn "New URI:$uri";
# Only re-write a URI if there are params or if it already hasn't been re-written
#unless (($cgi->param('r')) || (!$cgi->param()) ) {
# print $cgi->redirect( -uri=>$uri."&r=1",
# -cookie => $cookie);
# exit;
#}
# load the branches
my $branches = GetBranches();
@ -295,6 +273,9 @@ foreach my $advanced_srch_type (@advanced_search_types) {
}
}
$template->param(advancedsearchesloop => \@advancedsearchesloop);
my $types = C4::Context->preference("AdvancedSearchTypes") || "itemtypes";
my $advancedsearchesloop = prepare_adv_search_types($types);
$template->param(advancedsearchesloop => $advancedsearchesloop);
# The following should only be loaded if we're bringing up the advanced search template
if ( $template_type eq 'advsearch' ) {
@ -486,8 +467,21 @@ my $expanded_facet = $params->{'expand'};
# Define some global variables
my ( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
my $builder = Koha::SearchEngine::QueryBuilder->new(
{ index => $Koha::SearchEngine::BIBLIOS_INDEX } );
my $searcher = Koha::SearchEngine::Search->new(
{ index => $Koha::SearchEngine::BIBLIOS_INDEX } );
my @results;
## I. BUILD THE QUERY
( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type) = buildQuery(\@operators,\@operands,\@indexes,\@limits,\@sort_by,$scan,$lang);
(
$error, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
$query_type
)
= $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
\@sort_by, $scan, $lang );
## parse the query_cgi string and put it into a form suitable for <input>s
my @query_inputs;
@ -529,15 +523,14 @@ my $facets; # this object stores the faceted results that display on the left-ha
my $results_hashref;
eval {
($error, $results_hashref, $facets) = getRecords($query,$simple_query,\@sort_by,\@servers,$results_per_page,$offset,$expanded_facet,$branches,$itemtypes,$query_type,$scan);
my $itemtypes = GetItemTypes;
( $error, $results_hashref, $facets ) = $searcher->search_compat(
$query, $simple_query, \@sort_by, \@servers,
$results_per_page, $offset, $expanded_facet, $branches,
$itemtypes, $query_type, $scan
);
};
# This sorts the facets into alphabetical order
if ($facets) {
foreach my $f (@$facets) {
$f->{facets} = [ sort { uc($a->{facet_label_value}) cmp uc($b->{facet_label_value}) } @{ $f->{facets} } ];
}
}
if ($@ || $error) {
$template->param(query_error => $error.$@);
output_html_with_http_headers $cgi, $cookie, $template->output;
@ -767,3 +760,86 @@ $template->param(
);
output_html_with_http_headers $cgi, $cookie, $template->output;
=head2 prepare_adv_search_types
my $type = C4::Context->preference("AdvancedSearchTypes") || "itemtypes";
my @advanced_search_types = prepare_adv_search_types($type);
Different types can be searched for in the advanced search. This takes the
system preference that defines these types and parses it into an arrayref for
the template.
"itemtypes" is handled specially, as itemtypes aren't an authorised value.
It also accounts for the "item-level_itypes" system preference.
=cut
sub prepare_adv_search_types {
my ($types) = @_;
my @advanced_search_types = split( /\|/, $types );
# the index parameter is different for item-level itemtypes
my $itype_or_itemtype =
( C4::Context->preference("item-level_itypes") ) ? 'itype' : 'itemtype';
my $itemtypes = GetItemTypes;
my ( $cnt, @result );
foreach my $advanced_srch_type (@advanced_search_types) {
$advanced_srch_type =~ s/^\s*//;
$advanced_srch_type =~ s/\s*$//;
if ( $advanced_srch_type eq 'itemtypes' ) {
# itemtype is a special case, since it's not defined in authorized values
my @itypesloop;
foreach my $thisitemtype (
sort {
$itemtypes->{$a}->{'description'}
cmp $itemtypes->{$b}->{'description'}
} keys %$itemtypes
)
{
my %row = (
number => $cnt++,
ccl => "$itype_or_itemtype,phr",
code => $thisitemtype,
description => $itemtypes->{$thisitemtype}->{'description'},
imageurl => getitemtypeimagelocation(
'intranet', $itemtypes->{$thisitemtype}->{'imageurl'}
),
);
push @itypesloop, \%row;
}
my %search_code = (
advanced_search_type => $advanced_srch_type,
code_loop => \@itypesloop
);
push @result, \%search_code;
}
else {
# covers all the other cases: non-itemtype authorized values
my $advsearchtypes = GetAuthorisedValues($advanced_srch_type);
my @authvalueloop;
for my $thisitemtype (@$advsearchtypes) {
my %row = (
number => $cnt++,
ccl => $advanced_srch_type,
code => $thisitemtype->{authorised_value},
description => $thisitemtype->{'lib'},
imageurl => getitemtypeimagelocation(
'intranet', $thisitemtype->{'imageurl'}
),
);
push @authvalueloop, \%row;
}
my %search_code = (
advanced_search_type => $advanced_srch_type,
code_loop => \@authvalueloop
);
push @result, \%search_code;
}
}
return \@result;
}

View File

@ -34,6 +34,9 @@ use C4::Output;
use C4::Koha;
use C4::Search;
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
my $input = new CGI;
my $success = $input->param('biblioitem');
@ -74,15 +77,17 @@ if ($query) {
my $QParser;
$QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
my $builtquery;
my $builder = Koha::SearchEngine::QueryBuilder->new(
{ index => $Koha::SearchEngine::BIBLIOS_INDEX } );
my $searcher = Koha::SearchEngine::Search->new(
{ index => $Koha::SearchEngine::BIBLIOS_INDEX } );
if ($QParser) {
$builtquery = $query;
} else {
my ( $builterror,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
( $builterror,$builtquery,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type) = buildQuery(undef,\@operands);
( undef,$builtquery,undef,undef,undef,undef,undef,undef,undef,undef) = $builder->build_query_compat(undef,\@operands);
}
# find results
my ( $error, $marcresults, $total_hits ) = SimpleSearch($builtquery, $results_per_page * ($page - 1), $results_per_page);
my ( $error, $marcresults, $total_hits ) = $searcher->simple_search_compat($builtquery, $results_per_page * ($page - 1), $results_per_page);
if ( defined $error ) {
$template->param( error => $error );

View File

@ -35,6 +35,9 @@ use C4::Branch;
use Koha::ItemTypes;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
my $builder = sub {
my ( $params ) = @_;
my $function_name = $params->{id};
@ -179,8 +182,13 @@ my $launcher = sub {
} else {
$op = 'and';
}
my $searcher = Koha::SearchEngine::Search->new(
{ index => $Koha::SearchEngine::BIBLIOS_INDEX } );
$search = 'kw:' . $search . " $op mc-itemtype:" . $itype if $itype;
my ( $errors, $results, $total_hits ) = SimpleSearch( $search, $startfrom * $resultsperpage, $resultsperpage );
my ( $errors, $results, $total_hits ) =
$searcher->simple_search_compat( $search,
$startfrom * $resultsperpage,
$resultsperpage );
if ( defined $errors ) {
$results = [];
}

View File

@ -35,6 +35,14 @@ use C4::Branch; # GetBranches
use Koha::ItemTypes;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
sub plugin_parameters {
my ( $dbh, $record, $tagslib, $i, $tabloop ) = @_;
return "";
}
sub plugin_javascript {
my ( $dbh, $record, $tagslib, $field_number, $tabloop ) = @_;
my $function_name = $field_number;
@ -361,7 +369,8 @@ sub plugin {
$op = 'and';
}
$search = 'kw:'.$search." $op mc-itemtype:".$itype if $itype;
my ( $errors, $results, $total_hits ) = SimpleSearch($search, $startfrom * $resultsperpage, $resultsperpage );
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat($search, $startfrom * $resultsperpage, $resultsperpage );
if (defined $errors ) {
$results = [];
}

View File

@ -49,6 +49,8 @@ use Koha::DateUtils;
use Koha::Database;
use Koha::Patron::Messages;
use Koha::Patron::Images;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
use Date::Calc qw(
Today
@ -353,8 +355,9 @@ if (@$barcodes) {
{
$template_params->{FALLBACK} = 1;
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my $query = "kw=" . $barcode;
my ( $searcherror, $results, $total_hits ) = SimpleSearch($query);
my ( $searcherror, $results, $total_hits ) = $searcher->simple_search_compat($query, 0, 10);
# if multiple hits, offer options to librarian
if ( $total_hits > 0 ) {

View File

@ -0,0 +1,88 @@
#!/usr/bin/perl
use Modern::Perl;
use C4::Context;
use YAML::Syck;
use Koha::SearchMarcMaps;
use Koha::SearchFields;
my $dbh = C4::Context->dbh;
$dbh->do(q|INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type)
VALUES('SearchEngine','Zebra','Choose format to display postal addresses','','Choice')|);
$dbh->do(q|DROP TABLE IF EXISTS search_marc_to_field|);
$dbh->do(q|DROP TABLE IF EXISTS search_marc_map|);
$dbh->do(q|DROP TABLE IF EXISTS search_field|);
# This specifies the fields that will be stored in the search engine.
$dbh->do(q|
CREATE TABLE `search_field` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT 'the name of the field as it will be stored in the search engine',
`label` varchar(255) NOT NULL COMMENT 'the human readable name of the field, for display',
`type` ENUM('string', 'date', 'number', 'boolean', 'sum') NOT NULL COMMENT 'what type of data this holds, relevant when storing it in the search engine',
PRIMARY KEY (`id`),
UNIQUE KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
|);
# This contains a MARC field specifier for a given index, marc type, and marc
# field.
$dbh->do(q|
CREATE TABLE `search_marc_map` (
id int(11) NOT NULL AUTO_INCREMENT,
index_name ENUM('biblios','authorities') NOT NULL COMMENT 'what storage index this map is for',
marc_type ENUM('marc21', 'unimarc', 'normarc') NOT NULL COMMENT 'what MARC type this map is for',
marc_field VARCHAR(255) NOT NULL COMMENT 'the MARC specifier for this field',
PRIMARY KEY(`id`),
unique key( index_name, marc_field, marc_type),
INDEX (`index_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
|);
# This joins the two search tables together. We can have any combination:
# one marc field could have many search fields (maybe you want one value
# to go to 'author' and 'corporate-author) and many marc fields could go
# to one search field (e.g. all the various author fields going into
# 'author'.)
#
# a note about the sort field:
# * if all the entries for a mapping are 'null', nothing special is done with that mapping.
# * if any of the entries are not null, then a __sort field is created in ES for this mapping. In this case:
# * any mapping with sort == false WILL NOT get copied into a __sort field
# * any mapping with sort == true or is null WILL get copied into a __sort field
# * any sorts on the field name will be applied to $fieldname.'__sort' instead.
# this means that we can have search for author that includes 1xx, 245$c, and 7xx, but the sort only applies to 1xx.
$dbh->do(q|
CREATE TABLE `search_marc_to_field` (
search_marc_map_id int(11) NOT NULL,
search_field_id int(11) NOT NULL,
facet boolean DEFAULT FALSE COMMENT 'true if a facet field should be generated for this',
suggestible boolean DEFAULT FALSE COMMENT 'true if this field can be used to generate suggestions for browse',
sort boolean DEFAULT NULL COMMENT 'true/false creates special sort handling, null doesn''t',
PRIMARY KEY(search_marc_map_id, search_field_id),
FOREIGN KEY(search_marc_map_id) REFERENCES search_marc_map(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(search_field_id) REFERENCES search_field(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
|);
my $mappings_yaml = C4::Context->config('intranetdir') . '/admin/searchengine/elasticsearch/mappings.yaml';
my $indexes = LoadFile( $mappings_yaml );
while ( my ( $index_name, $fields ) = each %$indexes ) {
while ( my ( $field_name, $data ) = each %$fields ) {
my $field_type = $data->{type};
my $field_label = $data->{label};
my $mappings = $data->{mappings};
my $search_field = Koha::SearchFields->find_or_create({ name => $field_name, label => $field_label, type => $field_type }, { key => 'name' });
for my $mapping ( @$mappings ) {
my $marc_field = Koha::SearchMarcMaps->find_or_create({ index_name => $index_name, marc_type => $mapping->{marc_type}, marc_field => $mapping->{marc_field} });
$search_field->add_to_search_marc_maps($marc_field, { facet => $mapping->{facet}, suggestible => $mapping->{suggestible}, sort => $mapping->{sort} } );
}
}
}

View File

@ -1946,6 +1946,19 @@ CREATE TABLE saved_reports (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table 'search_field'
--
DROP TABLE IF EXISTS search_field;
CREATE TABLE `search_field` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT 'the name of the field as it will be stored in the search engine',
`label` varchar(255) NOT NULL COMMENT 'the human readable name of the field, for display',
`type` ENUM('string', 'date', 'number', 'boolean', 'sum') NOT NULL COMMENT 'what type of data this holds, relevant when storing it in the search engine',
PRIMARY KEY (`id`),
UNIQUE KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table `search_history`
@ -1966,6 +1979,36 @@ CREATE TABLE IF NOT EXISTS `search_history` ( -- patron's opac search history
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Opac search history results';
--
-- Table structure for table 'search_marc_map'
--
DROP TABLE IF EXISTS search_marc_map;
CREATE TABLE `search_marc_map` (
id int(11) NOT NULL AUTO_INCREMENT,
index_name ENUM('biblios','authorities') NOT NULL COMMENT 'what storage index this map is for',
marc_type ENUM('marc21', 'unimarc', 'normarc') NOT NULL COMMENT 'what MARC type this map is for',
marc_field VARCHAR(255) NOT NULL COMMENT 'the MARC specifier for this field',
PRIMARY KEY(`id`),
UNIQUE key(index_name, marc_field, marc_type),
INDEX (`index_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table 'search_marc_to_field'
--
DROP TABLE IF EXISTS search_marc_to_field;
CREATE TABLE `search_marc_to_field` (
search_marc_map_id int(11) NOT NULL,
search_field_id int(11) NOT NULL,
facet boolean DEFAULT FALSE COMMENT 'true if a facet field should be generated for this',
suggestible boolean DEFAULT FALSE COMMENT 'true if this field can be used to generate suggestions for browse',
sort boolean DEFAULT NULL COMMENT 'true/false creates special sort handling, null doesn''t',
PRIMARY KEY(search_marc_map_id, search_field_id),
FOREIGN KEY(search_marc_map_id) REFERENCES search_marc_map(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(search_field_id) REFERENCES search_field(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
--
-- Table structure for table `serial`

View File

@ -421,6 +421,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('RoutingSerials','1',NULL,'If ON, serials routing is enabled','YesNo'),
('SCOUserCSS','',NULL,'Add CSS to be included in the SCO module in an embedded <style> tag.','free'),
('SCOUserJS','',NULL,'Define custom javascript for inclusion in the SCO module','free'),
('SearchEngine','Zebra','Elasticsearch|Zebra','Search Engine','Choice'),
('SearchMyLibraryFirst','0',NULL,'If ON, OPAC searches return results limited by the user\'s library by default if they are logged in','YesNo'),
('SearchWithISBNVariations','0',NULL,'If enabled, search on all variations of the ISBN','YesNo'),
('SelfCheckHelpMessage','','70|10','Enter HTML to include under the basic Web-based Self Checkout instructions on the Help page','Textarea'),

View File

@ -12255,7 +12255,6 @@ if ( $column_has_been_used ) {
SetVersion($DBversion);
}
# DEVELOPER PROCESS, search for anything to execute in the db_update directory
# SEE bug 13068
# if there is anything in the atomicupdate, read and execute it.

View File

@ -211,3 +211,11 @@ Administration:
subscription: "subscription"
- will be shown on the <a href="http://hea.koha-community.org">Hea Koha community website</a>.
- Note that this value has no effect if the UsageStats system preference is set to "Don't share"
Search Engine:
-
- pref: SearchEngine
default: Zebra
choices:
Zebra: Zebra
Elasticsearch: Elasticsearch
- is the search engine used.

View File

@ -36,6 +36,8 @@ use C4::Creators::Lib qw(html_table);
use C4::Debug;
use Koha::DateUtils;
use Koha::SearchEngine::Search;
BEGIN {
$debug = $debug || $cgi_debug;
if ($debug) {
@ -107,14 +109,14 @@ if ( $op eq "do_search" ) {
}
my $offset = $startfrom > 1 ? $startfrom - 1 : 0;
( $error, $marcresults, $total_hits ) =
SimpleSearch( $ccl_query, $offset, $resultsperpage );
my $searcher = Koha::SearchEngine::Search->new({index => 'biblios'});
( $error, $marcresults, $total_hits ) = $searcher->simple_search_compat($ccl_query, $offset, $resultsperpage);
if (!defined $error && @{$marcresults} ) {
$show_results = @{$marcresults};
}
else {
$debug and warn "ERROR label-item-search: no results from SimpleSearch";
$debug and warn "ERROR label-item-search: no results from simple_search_compat";
# leave $show_results undef
}

View File

@ -31,6 +31,9 @@ use Getopt::Long;
use IO::File;
use Pod::Usage;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
use open qw( :std :encoding(UTF-8) );
binmode( STDOUT, ":encoding(UTF-8)" );
my ( $input_marc_file, $number, $offset) = ('',0,0);
@ -215,6 +218,17 @@ if ($logfile){
$loghandle= IO::File->new($logfile, $writemode) ;
print $loghandle "id;operation;status\n";
}
my $searcher = Koha::SearchEngine::Search->new(
{
index => (
$authorities
? $Koha::SearchEngine::AUTHORITIES_INDEX
: $Koha::SearchEngine::BIBLIOS_INDEX
)
}
);
RECORD: while ( ) {
my $record;
# get records
@ -268,7 +282,7 @@ RECORD: while ( ) {
my $query = build_query( $match, $record );
my $server = ( $authorities ? 'authorityserver' : 'biblioserver' );
$debug && warn $query;
my ( $error, $results, $totalhits ) = C4::Search::SimpleSearch( $query, 0, 3, [$server] );
my ( $error, $results, $totalhits ) = $searcher->simple_search_compat( $query, 0, 3, [$server] );
# changed to warn so able to continue with one broken record
if ( defined $error ) {
warn "unable to search the database for duplicates : $error";

View File

@ -34,9 +34,10 @@ BEGIN {
# Koha modules used
use MARC::Record;
use C4::Context;
use C4::Search;
use Getopt::Long;
use Koha::SearchEngine::Search;
my ( $help, $confirm, $zebraqueue, $silent,$stealth );
GetOptions(
@ -62,13 +63,14 @@ $| = 1;
my $sth = $dbh->prepare("SELECT biblionumber FROM biblio");
my $sth_insert = $dbh->prepare("INSERT INTO zebraqueue (biblio_auth_number,operation,server,done) VALUES (?,'specialUpdate','biblioserver',0)");
my $searcher = Koha::SearchEngine::Search->new({index => 'biblios'});
# We get all biblios
$sth->execute;
my ($nbhits);
# We check for each biblio
while ( my ($biblionumber) = $sth->fetchrow ) {
(undef,undef,$nbhits) = SimpleSearch("Local-number=$biblionumber");
(undef,undef,$nbhits) = $searcher->simple_search_compat("Local-number=$biblionumber");
print "biblionumber $biblionumber not indexed\n" unless $nbhits || $stealth;
# If -z option we put the biblio in zebraqueue
if ($zebraqueue && !$nbhits){
@ -105,4 +107,4 @@ Syntax:
\t./batchCheckNonIndexedBiblios.pl -s (s like silent => don't throws a warn every 1000 biblios to show progress)
\t./batchCheckNonIndexedBiblios.pl -st (s like stealth => don't throws a warn every 1000 biblios to show progress and no warn for the non indexed biblionumbers, just the total)
_USAGE_
}
}

View File

@ -27,6 +27,8 @@ use C4::Context;
use C4::AuthoritiesMarc;
use Getopt::Long;
use Koha::SearchEngine::Search;
my @authtypes;
my $want_help = 0;
my $test = 0;
@ -66,11 +68,12 @@ $rqselect->execute;
my $counter=0;
my $totdeleted=0;
my $totundeleted=0;
my $searcher = Koha::SearchEngine::Search->new({index => 'biblios'});
while (my $data=$rqselect->fetchrow_hashref){
my $query;
$query= "an=".$data->{'authid'};
# search for biblios mapped
my ($err,$res,$used) = C4::Search::SimpleSearch($query,0,10);
my ($err,$res,$used) = $searcher->simple_search_compat($query,0,10);
if (defined $err) {
warn "error: $err on search $query\n";
next;

View File

@ -0,0 +1,212 @@
#!/usr/bin/perl
# This inserts records from a Koha database into elastic search
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
rebuild_elastic_search.pl - inserts records from a Koha database into Elasticsearch
=head1 SYNOPSIS
B<rebuild_elastic_search.pl>
[B<-c|--commit>=C<count>]
[B<-v|--verbose>]
[B<-h|--help>]
[B<--man>]
=head1 DESCRIPTION
Inserts records from a Koha database into Elasticsearch.
=head1 OPTIONS
=over
=item B<-c|--commit>=C<count>
Specify how many records will be batched up before they're added to Elasticsearch.
Higher should be faster, but will cause more RAM usage. Default is 5000.
=item B<-d|--delete>
Delete the index and recreate it before indexing.
=item B<-a|--authorities>
Index the authorities only. Combining this with B<-b> is the same as
specifying neither and so both get indexed.
=item B<-b|--biblios>
Index the biblios only. Combining this with B<-a> is the same as
specifying neither and so both get indexed.
=item B<-bn|--bnumber>
Only index the supplied biblionumber, mostly for testing purposes. May be
repeated. This also applies to authorities via authid, so if you're using it,
you probably only want to do one or the other at a time.
=item B<-v|--verbose>
By default, this program only emits warnings and errors. This makes it talk
more. Add more to make it even more wordy, in particular when debugging.
=item B<-h|--help>
Help!
=item B<--man>
Full documentation.
=back
=cut
use autodie;
use Getopt::Long;
use C4::Context;
use Koha::Authority;
use Koha::BiblioUtils;
use Koha::ElasticSearch::Indexer;
use MARC::Field;
use MARC::Record;
use Modern::Perl;
use Pod::Usage;
use Data::Dumper; # TODO remove
my $verbose = 0;
my $commit = 5000;
my ($delete, $help, $man);
my ($index_biblios, $index_authorities);
my (@biblionumbers);
GetOptions(
'c|commit=i' => \$commit,
'd|delete' => \$delete,
'a|authorities' => \$index_authorities,
'b|biblios' => \$index_biblios,
'bn|bnumber=i' => \@biblionumbers,
'v|verbose+' => \$verbose,
'h|help' => \$help,
'man' => \$man,
);
# Default is to do both
unless ($index_authorities || $index_biblios) {
$index_authorities = $index_biblios = 1;
}
pod2usage(1) if $help;
pod2usage( -exitstatus => 0, -verbose => 2 ) if $man;
sanity_check();
my $next;
if ($index_biblios) {
_log(1, "Indexing biblios\n");
if (@biblionumbers) {
$next = sub {
my $r = shift @biblionumbers;
return () unless defined $r;
return ($r, Koha::BiblioUtils->get_from_biblionumber($r, item_data => 1 ));
};
} else {
my $records = Koha::BiblioUtils->get_all_biblios_iterator();
$next = sub {
$records->next();
}
}
do_reindex($next, $Koha::ElasticSearch::BIBLIOS_INDEX);
}
if ($index_authorities) {
_log(1, "Indexing authorities\n");
if (@biblionumbers) {
$next = sub {
my $r = shift @biblionumbers;
return () unless defined $r;
my $a = Koha::Authority->get_from_authid($r);
return ($r, $a->record);
};
} else {
my $records = Koha::Authority->get_all_authorities_iterator();
$next = sub {
$records->next();
}
}
do_reindex($next, $Koha::ElasticSearch::AUTHORITIES_INDEX);
}
sub do_reindex {
my ( $next, $index_name ) = @_;
my $indexer = Koha::ElasticSearch::Indexer->new( { index => $index_name } );
if ($delete) {
# We know it's safe to not recreate the indexer because update_index
# hasn't been called yet.
$indexer->drop_index();
}
my $count = 0;
my $commit_count = $commit;
my ( @id_buffer, @commit_buffer );
while ( my $record = $next->() ) {
my $id = $record->id;
my $record = $record->record;
_log( 1, "$id\n" );
$count++;
push @id_buffer, $id;
push @commit_buffer, $record;
if ( !( --$commit_count ) ) {
_log( 2, "Committing...\n" );
$indexer->update_index( \@id_buffer, \@commit_buffer );
$commit_count = $commit;
@id_buffer = ();
@commit_buffer = ();
}
}
# There are probably uncommitted records
$indexer->update_index( \@id_buffer, \@commit_buffer );
_log( 1, "$count records indexed.\n" );
}
# Checks some basic stuff to ensure that it's sane before we start.
sub sanity_check {
# Do we have an elasticsearch block defined?
my $conf = C4::Context->config('elasticsearch');
die "No 'elasticsearch' block is defined in koha-conf.xml.\n" if ( !$conf );
}
# Output progress information.
#
# _log($level, $msg);
#
# Will output $msg if the verbosity setting is set to $level or more. Will
# not include a trailing newline.
sub _log {
my ($level, $msg) = @_;
print $msg if ($verbose >= $level);
}

View File

@ -32,6 +32,8 @@ use C4::Koha;
use C4::Search::History;
use Koha::Authority::Types;
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
my $query = new CGI;
my $op = $query->param('op') || '';
@ -58,10 +60,19 @@ if ( $op eq "do_search" ) {
$resultsperpage = $query->param('resultsperpage');
$resultsperpage = 20 if ( !defined $resultsperpage );
my @tags;
my ( $results, $total, @fields ) =
SearchAuthorities( \@marclist, \@and_or, \@excluding, \@operator,
\@value, $startfrom * $resultsperpage,
$resultsperpage, $authtypecode, $orderby );
my $builder = Koha::SearchEngine::QueryBuilder->new(
{ index => $Koha::SearchEngine::AUTHORITIES_INDEX } );
my $searcher = Koha::SearchEngine::Search->new(
{ index => $Koha::SearchEngine::AUTHORITIES_INDEX } );
my $search_query = $builder->build_authorities_query_compat( \@marclist, \@and_or,
\@excluding, \@operator, \@value, $authtypecode, $orderby );
# use Data::Dumper;
# die Dumper(\@marclist, \@and_or,
# \@excluding, \@operator, \@value, $authtypecode, $orderby, $query);
# The searchengine API expects pages to start at page 1
$startfrom = $startfrom // 0;
my ( $results, $total ) =
$searcher->search_auth_compat( $search_query, $startfrom+1, $resultsperpage );
( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => "opac-authoritiessearchresultlist.tt",
@ -116,8 +127,9 @@ if ( $op eq "do_search" ) {
$to = ( ( $startfrom + 1 ) * $resultsperpage );
}
unless (C4::Context->preference('OPACShowUnusedAuthorities')) {
my @usedauths = grep { $_->{used} > 0 } @$results;
$results = \@usedauths;
# TODO implement usage counts
# my @usedauths = grep { $_->{used} > 0 } @$results;
# $results = \@usedauths;
}
# Opac search history
@ -151,7 +163,6 @@ if ( $op eq "do_search" ) {
}
$template->param( result => $results ) if $results;
$template->param( FIELDS => \@fields );
$template->param( orderby => $orderby );
$template->param(
startfrom => $startfrom,

View File

@ -30,6 +30,17 @@ use Modern::Perl;
use C4::Context;
use List::MoreUtils q/any/;
use Data::Dumper; # TODO remove
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
my $searchengine = C4::Context->preference("SearchEngine");
my ($builder, $searcher);
#$searchengine = 'Zebra'; # XXX
$builder = Koha::SearchEngine::QueryBuilder->new({index => 'biblios'});
$searcher = Koha::SearchEngine::Search->new({index => 'biblios'});
use C4::Output;
use C4::Auth qw(:DEFAULT get_session);
use C4::Languages qw(getLanguages);
@ -525,7 +536,7 @@ my ($error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_
my @results;
## I. BUILD THE QUERY
( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type) = buildQuery(\@operators,\@operands,\@indexes,\@limits,\@sort_by, 0, $lang);
( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type) = $builder->build_query_compat(\@operators,\@operands,\@indexes,\@limits,\@sort_by, 0, $lang, { expanded_facet => $expanded_facet });
sub _input_cgi_parse {
my @elements;
@ -605,15 +616,8 @@ if ($tag) {
$pasarParams .= '&amp;simple_query=' . uri_escape_utf8($simple_query);
$pasarParams .= '&amp;query_type=' . uri_escape_utf8($query_type) if ($query_type);
eval {
($error, $results_hashref, $facets) = getRecords($query,$simple_query,\@sort_by,\@servers,$results_per_page,$offset,$expanded_facet,$branches,$itemtypes_nocategory,$query_type,$scan,1);
};
}
# This sorts the facets into alphabetical order
if ($facets) {
foreach my $f (@$facets) {
$f->{facets} = [ sort { uc($a->{facet_label_value}) cmp uc($b->{facet_label_value}) } @{ $f->{facets} } ];
}
@$facets = sort {$a->{expand} cmp $b->{expand}} @$facets;
($error, $results_hashref, $facets) = $searcher->search_compat($query,$simple_query,\@sort_by,\@servers,$results_per_page,$offset,$expanded_facet,$branches,$itemtypes,$query_type,$scan,1);
};
}
# use Data::Dumper; print STDERR "-" x 25, "\n", Dumper($results_hashref);

View File

@ -58,6 +58,9 @@ use C4::Search;
use C4::Biblio;
use C4::Debug;
use Koha::SearchEngine;
use Koha::SearchEngine::Search;
my $input = new CGI;
my $op = $input->param('op') || q{};
my $dbh = C4::Context->dbh;
@ -96,8 +99,9 @@ if ( $op eq "do_search" && $query ) {
$resultsperpage = $input->param('resultsperpage');
$resultsperpage = 20 if ( !defined $resultsperpage );
my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
my ( $error, $marcrecords, $total_hits ) =
SimpleSearch( $query, $startfrom * $resultsperpage, $resultsperpage );
$searcher->simple_search_compat( $query, $startfrom * $resultsperpage, $resultsperpage );
my $total = 0;
if ( defined $marcrecords ) {
$total = scalar @{$marcrecords};

46
t/Koha/ItemType.t Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/perl
#
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Test::More tests => 8;
BEGIN {
use_ok('Koha::ItemType');
}
my $data = {
itemtype => 'CODE',
description => 'description',
rentalcharge => 'rentalcharge',
imageurl => 'imageurl',
summary => 'summary',
checkinmsg => 'checkinmsg',
checkinmsgtype => 'checkinmsgtype',
};
my $type = Koha::ItemType->new($data);
is( $type->itemtype, 'CODE', 'itemtype/code' );
is( $type->description, 'description', 'description' );
is( $type->rentalcharge, 'rentalcharge', 'rentalcharge' );
is( $type->imageurl, 'imageurl', 'imageurl' );
is( $type->summary, 'summary', 'summary' );
is( $type->checkinmsg, 'checkinmsg', 'checkinmsg' );
is( $type->checkinmsgtype, 'checkinmsgtype', 'checkinmsgtype' );

View File

@ -1,5 +1,7 @@
#!/usr/bin/perl
#
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it

79
t/db_dependent/Koha/ItemTypes.t Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/perl
#
# Copyright 2014 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Koha 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 GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Test::More tests => 18;
use Data::Dumper;
use Koha::Database;
BEGIN {
use_ok('Koha::ItemType');
use_ok('Koha::ItemTypes');
}
my $database = Koha::Database->new();
my $schema = $database->schema();
$schema->txn_begin;
Koha::ItemType->new(
{
itemtype => 'type1',
description => 'description',
rentalcharge => '0.00',
imageurl => 'imageurl',
summary => 'summary',
checkinmsg => 'checkinmsg',
checkinmsgtype => 'checkinmsgtype',
}
)->store;
Koha::ItemType->new(
{
itemtype => 'type2',
description => 'description',
rentalcharge => '0.00',
imageurl => 'imageurl',
summary => 'summary',
checkinmsg => 'checkinmsg',
checkinmsgtype => 'checkinmsgtype',
}
)->store;
my $type = Koha::ItemTypes->find('type1');
ok( defined($type), 'first result' );
is( $type->itemtype, 'type1', 'itemtype/code' );
is( $type->description, 'description', 'description' );
is( $type->rentalcharge, '0.0000', 'rentalcharge' );
is( $type->imageurl, 'imageurl', 'imageurl' );
is( $type->summary, 'summary', 'summary' );
is( $type->checkinmsg, 'checkinmsg', 'checkinmsg' );
is( $type->checkinmsgtype, 'checkinmsgtype', 'checkinmsgtype' );
$type = Koha::ItemTypes->find('type2');
ok( defined($type), 'second result' );
is( $type->itemtype, 'type2', 'itemtype/code' );
is( $type->description, 'description', 'description' );
is( $type->rentalcharge, '0.0000', 'rentalcharge' );
is( $type->imageurl, 'imageurl', 'imageurl' );
is( $type->summary, 'summary', 'summary' );
is( $type->checkinmsg, 'checkinmsg', 'checkinmsg' );
is( $type->checkinmsgtype, 'checkinmsgtype', 'checkinmsgtype' );
$schema->txn_rollback;

View File

@ -0,0 +1,23 @@
# Copyright 2015 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>
use strict;
use warnings;
use Test::More tests => 1; # last test to print
use_ok('Koha::ElasticSearch');

View File

@ -0,0 +1,52 @@
# Copyright 2015 Catalyst IT
#
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use Test::More tests => 5; # last test to print
use MARC::Record;
use_ok('Koha::ElasticSearch::Indexer');
my $indexer;
ok(
$indexer = Koha::ElasticSearch::Indexer->new(
{
'nodes' => ['localhost:9200'],
'index' => 'mydb'
}
),
'Creating new indexer object'
);
my $marc_record = MARC::Record->new();
my $field = MARC::Field->new( '001', '1234567' );
$marc_record->append_fields($field);
$field = MARC::Field->new( '020', '', '', 'a' => '1234567890123' );
$marc_record->append_fields($field);
$field = MARC::Field->new( '245', '', '', 'a' => 'Title' );
$marc_record->append_fields($field);
my $records = [$marc_record];
ok( my $converted = $indexer->_convert_marc_to_json($records),
'Convert some records' );
is( $converted->count, 1, 'One converted record' );
ok( $indexer->update_index(undef,$records), 'Update Index' );

View File

@ -0,0 +1,48 @@
# Copyright 2015 Catalyst IT
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use Test::More tests => 10; # last test to print
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
my $builder = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'mydb' } );
use_ok('Koha::SearchEngine::Elasticsearch::Search');
ok(
my $searcher = Koha::SearchEngine::Elasticsearch::Search->new(
{ 'nodes' => ['localhost:9200'], 'index' => 'mydb' }
),
'Creating a Koha::ElasticSearch::Search object'
);
is( $searcher->index, 'mydb', 'Testing basic accessor' );
ok( my $query = $builder->build_query('easy'), 'Build a search query');
ok( my $results = $searcher->search( $query) , 'Do a search ' );
ok( my $marc = $searcher->json2marc( $results->first ), 'Convert JSON to MARC');
is (my $count = $searcher->count( $query ), 0 , 'Get a count of the results, without returning results ');
ok ($results = $searcher->search_compat( $query ), 'Test search_compat' );
ok (($results,$count) = $searcher->search_auth_compat ( $query ), 'Test search_auth_compat' );
is ( $count = $searcher->count_auth_use($searcher,1), 0, 'Testing count_auth_use');