Bug 8233 : SearchEngine: Add a Koha::SearchEngine module

First draft introducing solr into Koha :-)

List of files :
  $ tree t/searchengine/
  t/searchengine
  |-- 000_conn
  |   `-- conn.t
  |-- 001_search
  |   `-- search_base.t
  |-- 002_index
  |   `-- index_base.t
  |-- 003_query
  |   `-- buildquery.t
  |-- 004_config
  |   `-- load_config.t
  `-- indexes.yaml
  just do `prove -r t/searchengine/**/*.t`

  t/lib
  |-- Mocks
  |   `-- Context.pm
  `-- Mocks.pm
  provide a mock to SearchEngine syspref (set_zebra and set_solr).

  $ tree Koha/SearchEngine
  Koha/SearchEngine
  |-- Config.pm
  |-- ConfigRole.pm
  |-- FacetsBuilder.pm
  |-- FacetsBuilderRole.pm
  |-- Index.pm
  |-- IndexRole.pm
  |-- QueryBuilder.pm
  |-- QueryBuilderRole.pm
  |-- Search.pm
  |-- SearchRole.pm
  |-- Solr
  |   |-- Config.pm
  |   |-- FacetsBuilder.pm
  |   |-- Index.pm
  |   |-- QueryBuilder.pm
  |   `-- Search.pm
  |-- Solr.pm
  |-- Zebra
  |   |-- QueryBuilder.pm
  |   `-- Search.pm
  `-- Zebra.pm

How to install and configure Solr ?
  See the wiki page: http://wiki.koha-community.org/wiki/SearchEngine_Layer_RFC

http://bugs.koha-community.org/show_bug.cgi?id=8233
Signed-off-by: Chris Cormack <chris@bigballofwax.co.nz>
This commit is contained in:
Jonathan Druart 2012-06-13 11:48:22 +02:00 committed by Paul Poulain
parent 078ce11e11
commit 623f3a2c84
48 changed files with 2436 additions and 3 deletions

29
Koha/SearchEngine.pm Normal file
View File

@ -0,0 +1,29 @@
package Koha::SearchEngine;
use Moose;
use C4::Context;
use Koha::SearchEngine::Config;
has 'name' => (
is => 'ro',
default => sub {
C4::Context->preference('SearchEngine');
}
);
has config => (
is => 'rw',
lazy => 1,
default => sub {
Koha::SearchEngine::Config->new;
}
# lazy => 1,
# builder => '_build_config',
);
#sub _build_config {
# my ( $self ) = @_;
# Koha::SearchEngine::Config->new( $self->name );
#);
1;

View File

@ -0,0 +1,12 @@
package Koha::SearchEngine::Config;
use Moose;
use Moose::Util qw( apply_all_roles );
sub BUILD {
my $self = shift;
my $syspref = C4::Context->preference("SearchEngine");
apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Config" );
};
1;

View File

@ -0,0 +1,7 @@
package Koha::SearchEngine::ConfigRole;
use Moose::Role;
requires 'indexes', 'index', 'ressource_types';
1;

View File

@ -0,0 +1,12 @@
package Koha::SearchEngine::FacetsBuilder;
use Moose;
use Moose::Util qw( apply_all_roles );
sub BUILD {
my $self = shift;
my $syspref = C4::Context->preference("SearchEngine");
apply_all_roles( $self, "Koha::SearchEngine::${syspref}::FacetsBuilder" );
};
1;

View File

@ -0,0 +1,7 @@
package Koha::SearchEngine::FacetsBuilderRole;
use Moose::Role;
requires 'build_facets';
1;

View File

@ -0,0 +1,11 @@
package Koha::SearchEngine::Index;
use Moose;
use Moose::Util qw( apply_all_roles );
sub BUILD {
my $self = shift;
my $syspref = 'Solr';
apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Index" );
};
1;

View File

@ -0,0 +1,6 @@
package Koha::SearchEngine::IndexRole;
use Moose::Role;
requires 'index_record';
1;

View File

@ -0,0 +1,12 @@
package Koha::SearchEngine::QueryBuilder;
use Moose;
use Moose::Util qw( apply_all_roles );
sub BUILD {
my $self = shift;
my $syspref = C4::Context->preference("SearchEngine");
apply_all_roles( $self, "Koha::SearchEngine::${syspref}::QueryBuilder" );
};
1;

View File

@ -0,0 +1,7 @@
package Koha::SearchEngine::QueryBuilderRole;
use Moose::Role;
requires 'build_query';
1;

View File

@ -0,0 +1,12 @@
package Koha::SearchEngine::Search;
use Moose;
use C4::Context;
use Moose::Util qw( apply_all_roles );
sub BUILD {
my $self = shift;
my $syspref = C4::Context->preference("SearchEngine");
apply_all_roles( $self, "Koha::SearchEngine::${syspref}::Search" );
};
1;

View File

@ -0,0 +1,7 @@
package Koha::SearchEngine::SearchRole;
use Moose::Role;
requires 'search';
requires 'dosmth';
1;

45
Koha/SearchEngine/Solr.pm Normal file
View File

@ -0,0 +1,45 @@
package Koha::SearchEngine::Solr;
use Moose;
use Koha::SearchEngine::Config;
extends 'Koha::SearchEngine', 'Data::SearchEngine::Solr';
has '+url' => (
is => 'ro',
isa => 'Str',
# default => sub {
# C4::Context->preference('SolrAPI');
# },
lazy => 1,
builder => '_build_url',
required => 1
);
sub _build_url {
my ( $self ) = @_;
$self->config->SolrAPI;
}
has '+options' => (
is => 'ro',
isa => 'HashRef',
default => sub {
{
wt => 'json',
fl => '*,score',
fq => 'recordtype:biblio',
facets => 'true'
}
}
);
has indexes => (
is => 'ro',
lazy => 1,
default => sub {
# my $dbh => ...;
},
);
1;

View File

@ -0,0 +1,115 @@
package Koha::SearchEngine::Solr::Config;
use Modern::Perl;
use Moose::Role;
use YAML;
with 'Koha::SearchEngine::ConfigRole';
has index_config => (
is => 'rw',
lazy => 1,
builder => '_load_index_config_file',
);
has solr_config => (
is => 'rw',
lazy => 1,
builder => '_load_solr_config_file',
);
has index_filename => (
is => 'rw',
lazy => 1,
default => C4::Context->config("installdir") . qq{/etc/searchengine/solr/indexes.yaml},
);
has solr_filename => (
is => 'rw',
lazy => 1,
default => C4::Context->config("installdir") . qq{/etc/searchengine/solr/config.yaml},
);
sub _load_index_config_file {
my ( $self, $filename ) = @_;
$self->index_filename( $filename ) if defined $filename;
die "The config index file (" . $self->index_filename . ") for Solr is not exist" if not -e $self->index_filename;
return YAML::LoadFile($self->index_filename);
}
sub _load_solr_config_file {
my ( $self ) = @_;
die "The solr config index file (" . $self->solr_filename . ") for Solr is not exist" if not -e $self->solr_filename;
return YAML::LoadFile($self->solr_filename);
}
sub set_config_filename {
my ( $self, $filename ) = @_;
$self->index_config( $self->_load_index_config_file( $filename ) );
}
sub SolrAPI {
my ( $self ) = @_;
return $self->solr_config->{SolrAPI};
}
sub indexes { # FIXME Return index list if param not an hashref (string ressource_type)
my ( $self, $indexes ) = @_;
return $self->write( { indexes => $indexes } ) if defined $indexes;
return $self->index_config->{indexes};
}
sub index {
my ( $self, $code ) = @_;
my @index = map { ( $_->{code} eq $code ) ? $_ : () } @{$self->index_config->{indexes}};
return $index[0];
}
sub ressource_types {
my ( $self ) = @_;
my $config = $self->index_config;
return $config->{ressource_types};
}
sub sortable_indexes {
my ( $self ) = @_;
my @sortable_indexes = map { $_->{sortable} ? $_ : () } @{ $self->index_config->{indexes} };
return \@sortable_indexes;
}
sub facetable_indexes {
my ( $self ) = @_;
my @facetable_indexes = map { $_->{facetable} ? $_ : () } @{ $self->index_config->{indexes} };
return \@facetable_indexes;
}
sub reload {
my ( $self ) = @_;
$self->index_config( $self->_load_index_config_file );
}
sub write {
my ( $self, $values ) = @_;
my $r;
while ( my ( $k, $v ) = each %$values ) {
$r->{$k} = $v;
}
if ( not grep /^ressource_type$/, keys %$values ) {
$r->{ressource_types} = $self->ressource_types;
}
if ( not grep /^indexes$/, keys %$values ) {
$r->{indexes} = $self->indexes;
}
eval {
YAML::DumpFile( $self->index_filename, $r );
};
if ( $@ ) {
die "Failed to dump the index config into the specified file ($@)";
}
$self->reload;
}
1;

View File

@ -0,0 +1,41 @@
package Koha::SearchEngine::Solr::FacetsBuilder;
use Modern::Perl;
use Moose::Role;
with 'Koha::SearchEngine::FacetsBuilderRole';
sub build_facets {
my ( $self, $results, $facetable_indexes, $filters ) = @_;
my @facets_loop;
for my $index ( @$facetable_indexes ) {
my $index_name = $index->{type} . '_' . $index->{code};
my $facets = $results->facets->{'str_' . $index->{code}};
if ( @$facets > 1 ) {
my @values;
$index =~ m/^([^_]*)_(.*)$/;
for ( my $i = 0 ; $i < scalar(@$facets) ; $i++ ) {
my $value = $facets->[$i++];
my $count = $facets->[$i];
utf8::encode($value);
my $lib =$value;
push @values, {
'lib' => $lib,
'value' => $value,
'count' => $count,
'active' => ( $filters->{$index_name} and scalar( grep /"?\Q$value\E"?/, @{ $filters->{$index_name} } ) ) ? 1 : 0,
};
}
push @facets_loop, {
'indexname' => $index_name,
'label' => $index->{label},
'values' => \@values,
'size' => scalar(@values),
};
}
}
return @facets_loop;
}
1;

View File

@ -0,0 +1,100 @@
package Koha::SearchEngine::Solr::Index;
use Moose::Role;
with 'Koha::SearchEngine::IndexRole';
use Data::SearchEngine::Solr;
use Data::Dump qw(dump);
use List::MoreUtils qw(uniq);
use Koha::SearchEngine::Solr;
use C4::AuthoritiesMarc;
use C4::Biblio;
has searchengine => (
is => 'rw',
isa => 'Koha::SearchEngine::Solr',
default => sub { Koha::SearchEngine::Solr->new },
lazy => 1
);
sub optimize {
my ( $self ) = @_;
return $self->searchengine->_solr->optimize;
}
sub index_record {
my ($self, $recordtype, $recordids) = @_;
my $indexes = $self->searchengine->config->indexes;
my @records;
my $recordids_str = ref($recordids) eq 'ARRAY'
? join " ", @$recordids
: $recordids;
warn "IndexRecord called with $recordtype $recordids_str";
for my $id ( @$recordids ) {
my $record;
$record = GetAuthority( $id ) if $recordtype eq "authority";
$record = GetMarcBiblio( $id ) if $recordtype eq "biblio";
next unless ( $record );
my $index_values = {
recordid => $id,
recordtype => $recordtype,
};
warn "Indexing $recordtype $id";
for my $index ( @$indexes ) {
next if $index->{ressource_type} ne $recordtype;
my @values;
eval {
my $mappings = $index->{mappings};
for my $tag_subf_code ( sort @$mappings ) {
my ( $f, $sf ) = split /\$/, $tag_subf_code;
for my $field ( $record->field( $f ) ) {
if ( $field->is_control_field ) {
push @values, $field->data;
} else {
my @sfvals = $sf eq '*'
? map { $_->[1] } $field->subfields
: map { $_ } $field->subfield( $sf );
for ( @sfvals ) {
$_ = NormalizeDate( $_ ) if $index->{type} eq 'date';
push @values, $_ if $_;
}
}
}
}
@values = uniq (@values); #Removes duplicates
$index_values->{$index->{type}."_".$index->{code}} = \@values;
if ( $index->{sortable} ){
$index_values->{"srt_" . $index->{type} . "_".$index->{code}} = $values[0];
}
# Add index str for facets if it's not exist
if ( $index->{facetable} and @values > 0 and $index->{type} ne 'str' ) {
$index_values->{"str_" . $index->{code}} = $values[0];
}
};
if ( $@ ) {
chomp $@;
warn "Error during indexation : recordid $id, index $index->{code} ( $@ )";
}
}
my $solrrecord = Data::SearchEngine::Item->new(
id => "${recordtype}_$id",
score => 1,
values => $index_values,
);
push @records, $solrrecord;
}
$self->searchengine->add( \@records );
}
1;

View File

@ -0,0 +1,146 @@
package Koha::SearchEngine::Solr::QueryBuilder;
use Modern::Perl;
use Moose::Role;
with 'Koha::SearchEngine::QueryBuilderRole';
sub build_advanced_query {
my ($class, $indexes, $operands, $operators) = @_;
my $q = '';
my $i = 0;
my $index_name;
@$operands or return "*:*"; #push @$operands, "[* TO *]";
# Foreach operands
for my $kw (@$operands){
$kw =~ s/(\w*\*)/\L$1\E/g; # Lower case on words with right truncation
$kw =~ s/(\s*\w*\?+\w*\s*)/\L$1\E/g; # Lower case on words contain wildcard ?
$kw =~ s/([^\\]):/$1\\:/g; # escape colons if not already escaped
# First element
if ($i == 0){
if ( (my @x = eval {@$indexes} ) == 0 ){
# There is no index, then query is in first operand
$q = @$operands[0];
last;
}
# Catch index name if it's not 'all_fields'
if ( @$indexes[$i] ne 'all_fields' ) {
$index_name = @$indexes[$i];
}else{
$index_name = '';
}
# Generate index:operand
$q .= BuildTokenString($index_name, $kw);
$i = $i + 1;
next;
}
# And others
$index_name = @$indexes[$i] if @$indexes[$i];
my $operator = defined @$operators[$i-1] ? @$operators[$i-1] : 'AND';
given ( uc ( $operator ) ) {
when ('OR'){
$q .= BuildTokenString($index_name, $kw, 'OR');
}
when ('NOT'){
$q .= BuildTokenString($index_name, $kw, 'NOT');
}
default {
$q .= BuildTokenString($index_name, $kw, 'AND');
}
}
$i = $i + 1;
}
return $q;
}
sub BuildTokenString {
my ($index, $string, $operator) = @_;
my $r;
if ($index ne 'all_fields' && $index ne ''){
# Operand can contains an expression in brackets
if (
$string =~ / /
and not ( $string =~ /^\(.*\)$/ )
and not $string =~ /\[.*TO.*\]/ ) {
my @dqs; #double-quoted string
while ( $string =~ /"(?:[^"\\]++|\\.)*+"/g ) {
push @dqs, $&;
$string =~ s/\ *\Q$&\E\ *//; # Remove useless space before and after
}
my @words = defined $string ? split ' ', $string : undef;
my $join = join qq{ AND } , map {
my $value = $_;
if ( $index =~ /^date_/ ) {
#$value = C4::Search::Engine::Solr::buildDateOperand( $value ); TODO
}
( $value =~ /^"/ and $value ne '""'
and $index ne "emallfields"
and $index =~ /(txt_|ste_)/ )
? qq{em$index:$value}
: qq{$index:$value};
} (@dqs, @words);
$r .= qq{($join)};
} else {
if ( $index =~ /^date_/ ) {
#$string = C4::Search::Engine::Solr::buildDateOperand( $string ); TODO
}
$r = "$index:$string";
}
}else{
$r = $string;
}
return " $operator $r" if $operator;
return $r;
}
sub build_query {
my ($class, $query) = @_;
return "*:*" if not defined $query;
# Particular *:* query
if ($query eq '*:*'){
return $query;
}
$query =~ s/(\w*\*)/\L$1\E/g; # Lower case on words with right truncation
$query =~ s/(\s*\w*\?+\w*\s*)/\L$1\E/g; # Lower case on words contain wildcard ?
my @quotes; # Process colons in quotes
while ( $query =~ /'(?:[^'\\]++|\\.)*+'/g ) {
push @quotes, $&;
}
for ( @quotes ) {
my $replacement = $_;
$replacement =~ s/[^\\]\K:/\\:/g;
$query =~ s/$_/$replacement/;
}
$query =~ s/ : / \\: /g; # escape colons if " : "
my $new_query = $query;#C4::Search::Query::splitToken($query); TODO
$new_query =~ s/all_fields://g;
# Upper case for operators
$new_query =~ s/ or / OR /g;
$new_query =~ s/ and / AND /g;
$new_query =~ s/ not / NOT /g;
return $new_query;
}
1;

View File

@ -0,0 +1,108 @@
package Koha::SearchEngine::Solr::Search;
use Moose::Role;
with 'Koha::SearchEngine::SearchRole';
use Data::Dump qw(dump);
use XML::Simple;
use Data::SearchEngine::Solr;
use Data::Pagination;
use Data::SearchEngine::Query;
use Koha::SearchEngine::Solr;
has searchengine => (
is => 'rw',
isa => 'Koha::SearchEngine::Solr',
default => sub { Koha::SearchEngine::Solr->new },
lazy => 1
);
sub search {
my ( $self, $q, $filters, $params ) = @_;
$q ||= '*:*';
$filters ||= {};
my $page = defined $params->{page} ? $params->{page} : 1;
my $count = defined $params->{count} ? $params->{count} : 999999999;
my $sort = defined $params->{sort} ? $params->{sort} : 'score desc';
my $facets = defined $params->{facets} ? $params->{facets} : 0;
# Construct fl from $params->{fl}
# If "recordid" or "id" not exist, we push them
my $fl = join ",",
defined $params->{fl}
? (
@{$params->{fl}},
grep ( /^recordid$/, @{$params->{fl}} ) ? () : "recordid",
grep ( /^id$/, @{$params->{fl}} ) ? () : "id"
)
: ( "recordid", "id" );
my $recordtype = ref($filters->{recordtype}) eq 'ARRAY'
? $filters->{recordtype}[0]
: $filters->{recordtype}
if defined $filters && defined $filters->{recordtype};
if ( $facets ) {
$self->searchengine->options->{"facet"} = 'true';
$self->searchengine->options->{"facet.mincount"} = 1;
$self->searchengine->options->{"facet.limit"} = 10; # TODO create a new systempreference C4::Context->preference("numFacetsDisplay")
my @facetable_indexes = map { 'str_' . $_->{code} } @{$self->searchengine->config->facetable_indexes};
$self->searchengine->options->{"facet.field"} = \@facetable_indexes;
}
$self->searchengine->options->{sort} = $sort;
$self->searchengine->options->{fl} = $fl;
# Construct filters
$self->searchengine->options->{fq} = [
map {
my $idx = $_;
ref($filters->{$idx}) eq 'ARRAY'
?
'('
. join( ' AND ',
map {
my $filter_str = $_;
utf8::decode($filter_str);
my $quotes_existed = ( $filter_str =~ m/^".*"$/ );
$filter_str =~ s/^"(.*)"$/$1/; #remove quote around value if exist
$filter_str =~ s/[^\\]\K"/\\"/g;
$filter_str = qq{"$filter_str"} # Add quote around value if not exist
if not $filter_str =~ /^".*"$/
and $quotes_existed;
qq{$idx:$filter_str};
} @{ $filters->{$idx} } )
. ')'
: "$idx:$filters->{$idx}";
} keys %$filters
];
my $sq = Data::SearchEngine::Query->new(
page => $page,
count => $count,
query => $q,
);
# Get results
my $results = eval { $self->searchengine->search( $sq ) };
# Get error if exists
if ( $@ ) {
my $err = $@;
$err =~ s#^[^\n]*\n##; # Delete first line
if ( $err =~ "400 URL must be absolute" ) {
$err = "Your system preference 'SolrAPI' is not set correctly";
}
elsif ( not $err =~ 'Connection refused' ) {
my $document = XMLin( $err );
$err = "$$document{body}{h2} : $$document{body}{pre}";
}
$results->{error} = $err;
}
return $results;
}
sub dosmth {'bou' }
1;

View File

@ -0,0 +1,14 @@
package Koha::SearchEngine::Zebra;
use Moose;
extends 'Data::SearchEngine::Zebra';
# the configuration file is retrieved from KOHA_CONF by default, provide it from there²
has '+conf_file' => (
is => 'ro',
isa => 'Str',
default => $ENV{KOHA_CONF},
required => 1
);
1;

View File

@ -0,0 +1,14 @@
package Koha::SearchEngine::Zebra::QueryBuilder;
use Modern::Perl;
use Moose::Role;
use C4::Search;
with 'Koha::SearchEngine::QueryBuilderRole';
sub build_query {
shift;
C4::Search::buildQuery @_;
}
1;

View File

@ -0,0 +1,40 @@
package Koha::SearchEngine::Zebra::Search;
use Moose::Role;
with 'Koha::SearchEngine::SearchRole';
use Data::SearchEngine::Zebra;
use Data::SearchEngine::Query;
use Koha::SearchEngine::Zebra;
use Data::Dump qw(dump);
has searchengine => (
is => 'rw',
isa => 'Koha::SearchEngine::Zebra',
default => sub { Koha::SearchEngine::Zebra->new },
lazy => 1
);
sub search {
my ($self,$query_string) = @_;
my $query = Data::SearchEngine::Query->new(
count => 10,
page => 1,
query => $query_string,
);
warn "search for $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";
warn dump $title;
}
}
sub dosmth {'bou' }
1;

View File

@ -0,0 +1,103 @@
#!/usr/bin/perl
# Copyright 2012 BibLibre SARL
#
# 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 2 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 CGI;
use C4::Koha;
use C4::Output;
use C4::Auth;
use Koha::SearchEngine;
my $input = new CGI;
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => 'admin/searchengine/solr/indexes.tt',
query => $input,
type => 'intranet',
# authnotrequired => 0,
# flagsrequired => { reserveforothers => "place_holds" }, #TODO
}
);
my $ressource_type = $input->param('ressource_type') || 'biblio';
my $se = Koha::SearchEngine->new;
my $se_config = $se->config;
my $indexes;
if ( $input->param('op') and $input->param('op') eq 'edit' ) {
my @code = $input->param('code');
my @label = $input->param('label');
my @type = $input->param('type');
my @sortable = $input->param('sortable');
my @facetable = $input->param('facetable');
my @mandatory = $input->param('mandatory');
my @ressource_type = $input->param('ressource_type');
my @mappings = $input->param('mappings');
my @indexes;
my @errors;
for ( 0 .. @code-1 ) {
my $icode = $code[$_];
my @current_mappings = split /\r\n/, $mappings[$_];
if ( not @current_mappings ) {
@current_mappings = split /\n/, $mappings[$_];
}
if ( not @current_mappings ) {
push @errors, { type => 'no_mapping', value => $icode};
}
push @indexes, {
code => $icode,
label => $label[$_],
type => $type[$_],
sortable => scalar(grep(/^$icode$/, @sortable)),
facetable => scalar(grep(/^$icode$/, @facetable)),
mandatory => $mandatory[$_] eq '1' ? '1' : '0',
ressource_type => $ressource_type[$_],
mappings => \@current_mappings,
};
for my $m ( @current_mappings ) {
push @errors, {type => 'malformed_mapping', value => $m}
if not $m =~ /^\d(\d|\*|\.){2}\$.$/;
}
}
$indexes = \@indexes if @errors;
$template->param( errors => \@errors );
$se_config->indexes(\@indexes) if not @errors;
}
my $ressource_types = $se_config->ressource_types;
$indexes //= $se_config->indexes;
my $indexloop;
for my $rt ( @$ressource_types ) {
my @indexes = map {
$_->{ressource_type} eq $rt ? $_ : ();
} @$indexes;
push @$indexloop, {
ressource_type => $rt,
indexes => \@indexes,
}
}
$template->param(
indexloop => $indexloop,
);
output_html_with_http_headers $input, $cookie, $template->output;

View File

@ -0,0 +1 @@
SolrAPI: 'http://localhost:8983/solr/solr'

View File

@ -0,0 +1,45 @@
---
indexes:
- code: title
facetable: 1
label: Title
mandatory: 1
mappings:
- 200$a
- 210$a
- 4..$t
ressource_type: biblio
sortable: 1
type: ste
- code: author
facetable: 1
label: Author
mandatory: 0
mappings:
- 700$*
- 710$*
ressource_type: biblio
sortable: 1
type: str
- code: subject
facetable: 0
label: Subject
mandatory: 0
mappings:
- 600$a
- 601$a
ressource_type: biblio
sortable: 0
type: str
- code: biblionumber
facetable: 0
label: Biblionumber
mandatory: 1
mappings:
- 001$@
ressource_type: biblio
sortable: 0
type: int
ressource_types:
- biblio
- authority

View File

@ -0,0 +1,33 @@
ressource_types:
- biblio
- authority
indexes:
- code: title
label: Title
type: ste
ressource_type: biblio
sortable: 1
mandatory: 1
mappings:
- 200$a
- 210$a
- 4..$t
- code: author
label: Author
type: str
ressource_type: biblio
sortable: 1
mandatory: 0
mappings:
- 700$*
- 710$*
- code: subject
label: Subject
type: str
ressource_type: biblio
sortable: 0
mandatory: 0
mappings:
- 600$a
- 601$a

33
etc/solr/indexes.yaml Normal file
View File

@ -0,0 +1,33 @@
ressource_types:
- biblio
- authority
indexes:
- code: title
label: Title
type: ste
ressource_type: biblio
sortable: 1
mandatory: 1
mappings:
- 200$a
- 210$a
- 4..$t
- code: author
label: Author
type: str
ressource_type: biblio
sortable: 1
mandatory: 0
mappings:
- 700$*
- 710$*
- code: subject
label: Subject
type: str
ressource_type: biblio
sortable: 0
mandatory: 0
mappings:
- 600$a
- 601$a

View File

@ -5459,6 +5459,13 @@ if (C4::Context->preference("Version") < TransformToNum($DBversion)) {
SetVersion($DBversion);
}
$DBversion = "3.07.00.023";
if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
$dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES('SearchEngine','Zebra','Solr|Zebra','Search Engine','Choice')");
print "Upgrade to $DBversion done (Add system preference SearchEngine )\n";
SetVersion($DBversion);
}
=head1 FUNCTIONS
=head2 TableExists($table)

View File

@ -285,6 +285,10 @@ td {
vertical-align : top;
}
table.indexes td {
vertical-align : middle;
}
td.borderless {
border-collapse : separate;
border : 0 none;

View File

@ -0,0 +1,95 @@
/**
* TextAreaExpander plugin for jQuery
* v1.0
* Expands or contracts a textarea height depending on the
* quatity of content entered by the user in the box.
*
* By Craig Buckler, Optimalworks.net
*
* As featured on SitePoint.com:
* http://www.sitepoint.com/blogs/2009/07/29/build-auto-expanding-textarea-1/
*
* Please use as you wish at your own risk.
*/
/**
* Usage:
*
* From JavaScript, use:
* $(<node>).TextAreaExpander(<minHeight>, <maxHeight>);
* where:
* <node> is the DOM node selector, e.g. "textarea"
* <minHeight> is the minimum textarea height in pixels (optional)
* <maxHeight> is the maximum textarea height in pixels (optional)
*
* Alternatively, in you HTML:
* Assign a class of "expand" to any <textarea> tag.
* e.g. <textarea name="textarea1" rows="3" cols="40" class="expand"></textarea>
*
* Or assign a class of "expandMIN-MAX" to set the <textarea> minimum and maximum height.
* e.g. <textarea name="textarea1" rows="3" cols="40" class="expand50-200"></textarea>
* The textarea will use an appropriate height between 50 and 200 pixels.
*/
(function($) {
// jQuery plugin definition
$.fn.TextAreaExpander = function(minHeight, maxHeight) {
var hCheck = !($.browser.msie || $.browser.opera);
// resize a textarea
function ResizeTextarea(e) {
// event or initialize element?
e = e.target || e;
// find content length and box width
var vlen = e.value.length, ewidth = e.offsetWidth;
if (vlen != e.valLength || ewidth != e.boxWidth) {
if (hCheck && (vlen < e.valLength || ewidth != e.boxWidth)) e.style.height = "0px";
var h = Math.max(e.expandMin, Math.min(e.scrollHeight, e.expandMax));
e.style.overflow = (e.scrollHeight > h ? "auto" : "hidden");
e.style.height = h + "px";
e.valLength = vlen;
e.boxWidth = ewidth;
}
return true;
};
// initialize
this.each(function() {
// is a textarea?
if (this.nodeName.toLowerCase() != "textarea") return;
// set height restrictions
var p = this.className.match(/expand(\d+)\-*(\d+)*/i);
this.expandMin = minHeight || (p ? parseInt('0'+p[1], 10) : 0);
this.expandMax = maxHeight || (p ? parseInt('0'+p[2], 10) : 99999);
// initial resize
ResizeTextarea(this);
// zero vertical padding and add events
if (!this.Initialized) {
this.Initialized = true;
$(this).css("padding-top", 0).css("padding-bottom", 0);
$(this).bind("keyup", ResizeTextarea).bind("focus", ResizeTextarea);
}
});
return this;
};
})(jQuery);
// initialize all expanding textareas
jQuery(document).ready(function() {
jQuery("textarea[class*=expand]").TextAreaExpander();
});

View File

@ -0,0 +1,382 @@
/**
* TableDnD plug-in for JQuery, allows you to drag and drop table rows
* You can set up various options to control how the system will work
* Copyright (c) Denis Howlett <denish@isocra.com>
* Licensed like jQuery, see http://docs.jquery.com/License.
*
* Configuration options:
*
* onDragStyle
* This is the style that is assigned to the row during drag. There are limitations to the styles that can be
* associated with a row (such as you can't assign a border--well you can, but it won't be
* displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
* a map (as used in the jQuery css(...) function).
* onDropStyle
* This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
* to what you can do. Also this replaces the original style, so again consider using onDragClass which
* is simply added and then removed on drop.
* onDragClass
* This class is added for the duration of the drag and then removed when the row is dropped. It is more
* flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
* is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
* stylesheet.
* onDrop
* Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
* and the row that was dropped. You can work out the new order of the rows by using
* table.rows.
* onDragStart
* Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
* table and the row which the user has started to drag.
* onAllowDrop
* Pass a function that will be called as a row is over another row. If the function returns true, allow
* dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
* the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
* scrollAmount
* This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
* window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
* FF3 beta
* dragHandle
* This is the name of a class that you assign to one or more cells in each row that is draggable. If you
* specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
* will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
* the whole row is draggable.
*
* Other ways to control behaviour:
*
* Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
* that you don't want to be draggable.
*
* Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
* <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
* an ID as must all the rows.
*
* Other methods:
*
* $("...").tableDnDUpdate()
* Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
* This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
* The table maintains the original configuration (so you don't have to specify it again).
*
* $("...").tableDnDSerialize()
* Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
* called from anywhere and isn't dependent on the currentTable being set up correctly before calling
*
* Known problems:
* - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
*
* Version 0.2: 2008-02-20 First public version
* Version 0.3: 2008-02-07 Added onDragStart option
* Made the scroll amount configurable (default is 5 as before)
* Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
* Added onAllowDrop to control dropping
* Fixed a bug which meant that you couldn't set the scroll amount in both directions
* Added serialize method
* Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
* draggable
* Improved the serialize method to use a default (and settable) regular expression.
* Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
*/
jQuery.tableDnD = {
/** Keep hold of the current table being dragged */
currentTable : null,
/** Keep hold of the current drag object if any */
dragObject: null,
/** The current mouse offset */
mouseOffset: null,
/** Remember the old value of Y so that we don't do too much processing */
oldY: 0,
/** Actually build the structure */
build: function(options) {
// Set up the defaults if any
this.each(function() {
// This is bound to each matching table, set up the defaults and override with user options
this.tableDnDConfig = jQuery.extend({
onDragStyle: null,
onDropStyle: null,
// Add in the default class for whileDragging
onDragClass: "tDnD_whileDrag",
onDrop: null,
onDragStart: null,
scrollAmount: 5,
serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
serializeParamName: null, // If you want to specify another parameter name instead of the table ID
dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
}, options || {});
// Now make the rows draggable
jQuery.tableDnD.makeDraggable(this);
});
// Now we need to capture the mouse up and mouse move event
// We can use bind so that we don't interfere with other event handlers
jQuery(document)
.bind('mousemove', jQuery.tableDnD.mousemove)
.bind('mouseup', jQuery.tableDnD.mouseup);
// Don't break the chain
return this;
},
/** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
makeDraggable: function(table) {
var config = table.tableDnDConfig;
if (table.tableDnDConfig.dragHandle) {
// We only need to add the event to the specified cells
var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
cells.each(function() {
// The cell is bound to "this"
jQuery(this).mousedown(function(ev) {
jQuery.tableDnD.dragObject = this.parentNode;
jQuery.tableDnD.currentTable = table;
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
if (config.onDragStart) {
// Call the onDrop method if there is one
config.onDragStart(table, this);
}
return false;
});
})
} else {
// For backwards compatibility, we add the event to the whole row
var rows = jQuery("tr", table); // get all the rows as a wrapped set
rows.each(function() {
// Iterate through each row, the row is bound to "this"
var row = jQuery(this);
if (! row.hasClass("nodrag")) {
row.mousedown(function(ev) {
if (ev.target.tagName == "TD") {
jQuery.tableDnD.dragObject = this;
jQuery.tableDnD.currentTable = table;
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
if (config.onDragStart) {
// Call the onDrop method if there is one
config.onDragStart(table, this);
}
return false;
}
}).css("cursor", "move"); // Store the tableDnD object
}
});
}
},
updateTables: function() {
this.each(function() {
// this is now bound to each matching table
if (this.tableDnDConfig) {
jQuery.tableDnD.makeDraggable(this);
}
})
},
/** Get the mouse coordinates from the event (allowing for browser differences) */
mouseCoords: function(ev){
if(ev.pageX || ev.pageY){
return {x:ev.pageX, y:ev.pageY};
}
return {
x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
y:ev.clientY + document.body.scrollTop - document.body.clientTop
};
},
/** Given a target element and a mouse event, get the mouse offset from that element.
To do this we need the element's position and the mouse position */
getMouseOffset: function(target, ev) {
ev = ev || window.event;
var docPos = this.getPosition(target);
var mousePos = this.mouseCoords(ev);
return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
},
/** Get the position of an element by going up the DOM tree and adding up all the offsets */
getPosition: function(e){
var left = 0;
var top = 0;
/** Safari fix -- thanks to Luis Chato for this! */
if (e.offsetHeight == 0) {
/** Safari 2 doesn't correctly grab the offsetTop of a table row
this is detailed here:
http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
note that firefox will return a text node as a first child, so designing a more thorough
solution may need to take that into account, for now this seems to work in firefox, safari, ie */
e = e.firstChild; // a table cell
}
while (e.offsetParent){
left += e.offsetLeft;
top += e.offsetTop;
e = e.offsetParent;
}
left += e.offsetLeft;
top += e.offsetTop;
return {x:left, y:top};
},
mousemove: function(ev) {
if (jQuery.tableDnD.dragObject == null) {
return;
}
var dragObj = jQuery(jQuery.tableDnD.dragObject);
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
var mousePos = jQuery.tableDnD.mouseCoords(ev);
var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
//auto scroll the window
var yOffset = window.pageYOffset;
if (document.all) {
// Windows version
//yOffset=document.body.scrollTop;
if (typeof document.compatMode != 'undefined' &&
document.compatMode != 'BackCompat') {
yOffset = document.documentElement.scrollTop;
}
else if (typeof document.body != 'undefined') {
yOffset=document.body.scrollTop;
}
}
if (mousePos.y-yOffset < config.scrollAmount) {
window.scrollBy(0, -config.scrollAmount);
} else {
var windowHeight = window.innerHeight ? window.innerHeight
: document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
window.scrollBy(0, config.scrollAmount);
}
}
if (y != jQuery.tableDnD.oldY) {
// work out if we're going up or down...
var movingDown = y > jQuery.tableDnD.oldY;
// update the old value
jQuery.tableDnD.oldY = y;
// update the style to show we're dragging
if (config.onDragClass) {
dragObj.addClass(config.onDragClass);
} else {
dragObj.css(config.onDragStyle);
}
// If we're over a row then move the dragged row to there so that the user sees the
// effect dynamically
var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
if (currentRow) {
// TODO worry about what happens when there are multiple TBODIES
if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
} else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
}
}
}
return false;
},
/** We're only worried about the y position really, because we can only move rows up and down */
findDropTargetRow: function(draggedRow, y) {
var rows = jQuery.tableDnD.currentTable.rows;
for (var i=0; i<rows.length; i++) {
var row = rows[i];
var rowY = this.getPosition(row).y;
var rowHeight = parseInt(row.offsetHeight)/2;
if (row.offsetHeight == 0) {
rowY = this.getPosition(row.firstChild).y;
rowHeight = parseInt(row.firstChild.offsetHeight)/2;
}
// Because we always have to insert before, we need to offset the height a bit
if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
// that's the row we're over
// If it's the same as the current row, ignore it
if (row == draggedRow) {return null;}
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
if (config.onAllowDrop) {
if (config.onAllowDrop(draggedRow, row)) {
return row;
} else {
return null;
}
} else {
// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
var nodrop = jQuery(row).hasClass("nodrop");
if (! nodrop) {
return row;
} else {
return null;
}
}
return row;
}
}
return null;
},
mouseup: function(e) {
if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
var droppedRow = jQuery.tableDnD.dragObject;
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
// If we have a dragObject, then we need to release it,
// The row will already have been moved to the right place so we just reset stuff
if (config.onDragClass) {
jQuery(droppedRow).removeClass(config.onDragClass);
} else {
jQuery(droppedRow).css(config.onDropStyle);
}
jQuery.tableDnD.dragObject = null;
if (config.onDrop) {
// Call the onDrop method if there is one
config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
}
jQuery.tableDnD.currentTable = null; // let go of the table too
}
},
serialize: function() {
if (jQuery.tableDnD.currentTable) {
return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
} else {
return "Error: No Table id set, you need to set an id on your table and every row";
}
},
serializeTable: function(table) {
var result = "";
var tableId = table.id;
var rows = table.rows;
for (var i=0; i<rows.length; i++) {
if (result.length > 0) result += "&";
var rowId = rows[i].id;
if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
}
result += tableId + '[]=' + rowId;
}
return result;
},
serializeTables: function() {
var result = "";
this.each(function() {
// this is now bound to each matching table
result += jQuery.tableDnD.serializeTable(this);
});
return result;
}
}
jQuery.fn.extend(
{
tableDnD : jQuery.tableDnD.build,
tableDnDUpdate : jQuery.tableDnD.updateTables,
tableDnDSerialize: jQuery.tableDnD.serializeTables
}
);

View File

@ -76,6 +76,8 @@
<dd>Manage rules for automatically matching MARC records during record imports.</dd>
<dt><a href="/cgi-bin/koha/admin/oai_sets.pl">OAI sets configuration</a></dt>
<dd>Manage OAI Sets</dd>
<dt><a href="/cgi-bin/koha/admin/searchengine/solr/indexes.pl">Search engine configuration</a></dt>
<dd>Manage indexes, facets, and their mappings to MARC fields and subfields.</dd>
</dl>
<h3>Acquisition parameters</h3>

View File

@ -104,3 +104,11 @@ Administration:
Common Name: the Common Name
emailAddress: the emailAddress
- field for SSL client certificate authentication
Search Engine:
-
- pref: SearchEngine
default: Zebra
choices:
Solr: Solr
Zebra: Zebra
- is the search engine used.

View File

@ -0,0 +1,190 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Administration &rsaquo; Solr config</title>
[% INCLUDE 'doc-head-close.inc' %]
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/tablednd.js"></script>
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.textarea-expander.js"></script>
<script type="text/javascript" language="javascript">
function clean_line( line ) {
$(line).find('input[type="text"]').val("");
$(line).find('input[type="checkbox"]').attr("checked", false);
$(line).find('textarea').val("");
$(line).find('select').find('option:first').attr("selected", "selected");
}
function clone_line( line ) {
var new_line = $(line).clone();
$(new_line).removeClass("nodrag nodrop");
$(new_line).find('td:last-child>a').removeClass("add").addClass("delete").html(_("Delete"));
$(new_line).find('[data-id]').each( function() {
$(this).attr({ name: $(this).attr('data-id') }).removeAttr('data-id');
} );
$(new_line).find("select").each( function() {
var attr = $(this).attr('name');
var val = $(line).find('[data-id="' + attr + '"]').val();
$(this).find('option[value="' + val + '"]').attr("selected", "selected");
} );
return new_line;
}
$(document).ready(function() {
$('.delete').click(function() {
$(this).parents('tr').remove();
});
$(".indexes").tableDnD( {
onDragClass: "dragClass",
} );
$("textarea").TextAreaExpander();
$('.add').click(function() {
var table = $(this).closest('table');
var ressource_type = $(table).attr('data-ressource_type');
var code = $(table).find('input[data-id="code"]').val();
var label = $(table).find('input[data-id="label"]').val();
if ( code.length > 0 && label.length > 0 ) {
var line = $(this).closest("tr");
var mappings = $(line).find('textarea').val();
var new_line = clone_line( line );
$(new_line).find('textarea').val(mappings);
$(new_line).find("input:checkbox").val(code);
new_line.appendTo($('table[data-ressource_type="'+ressource_type+'"]>tbody'));
$('.delete').click(function() {
$(this).parents('tr').remove();
});
clean_line(line);
$(table).tableDnD( {
onDragClass: "dragClass",
} );
}
});
});
</script>
</head>
<body id="admin_searchengine_indexes" class="admin">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a> &rsaquo; Search engine configuration</div>
<div id="doc3" class="yui-t1">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<h1>Search engine configuration</h1>
<div class="warning">
Warning: Any modification in these configurations will need a total reindexation to be fully taken into account !
</div>
[% IF ( errors ) %]
<div class="error">
Errors occurred, Modifications does not apply. Please check following values:
<ul>
[% FOREACH e IN errors %]
<li>
[% IF ( e.type == "malformed_mapping" ) %]
The value "[% e.value %]" is not supported for mappings
[% ELSIF ( e.type == "no_mapping" ) %]
There is no mapping for the index [% e.value %]
[% END %]
</li>
[% END %]
</ul>
</div>
[% END %]
<form method="post">
<input type="hidden" name="op" value="edit" />
[% FOREACH rt IN indexloop %]
<h2>[% rt.ressource_type %]</h2>
[% IF ( rt.ressource_type == 'authority' ) %]
This part is not yet implemented
[% END %]
<table id="pouet" class="indexes" data-ressource_type="[% rt.ressource_type %]">
<thead>
<tr class="nodrag nodrop">
<th>Code</th>
<th>Label</th>
<th>Type</th>
<th>Sortable</th>
<th>Facetable</th>
<th>Mapping</th>
<th></th>
</tr>
</thead>
<tbody>
[% FOREACH index IN rt.indexes %]
<tr>
<td>
[% IF ( index.mandatory ) %]
<input name="code" type="text" maxlength="25" value="[% index.code %]" disabled="disabled" />
<input name="code" type="hidden" maxlength="25" value="[% index.code %]" />
[% ELSE %]
<input name="code" type="text" maxlength="25" value="[% index.code %]" />
[% END %]
<input name="mandatory" type="hidden" maxlength="1" value="[% index.mandatory %]" />
<input name="ressource_type" type="hidden" value="[% index.ressource_type %]" />
</td>
<td><input name="label" type="text" maxlength="25" value="[% index.label %]" /></td>
<td>
[% IF ( index.mandatory ) %]
<input type="hidden" name="type" value="[% index.type %]" />
[% END %]
<select name="type"[% IF ( index.mandatory ) %] disabled="disabled"[% END %]>
<option [% IF ( index.type == 'str' ) %] selected="selected"[% END %] value="str">String</option>
<option [% IF ( index.type == 'ste' ) %] selected="selected"[% END %] value="ste">Simple Text</option>
<option [% IF ( index.type == 'txt' ) %] selected="selected"[% END %] value="txt">Text</option>
<option [% IF ( index.type == 'int' ) %] selected="selected"[% END %] value="int">Integer</option>
<option [% IF ( index.type == 'date') %] selected="selected"[% END %] value="date">Date</option>
</select>
</td>
<td>
<input name="sortable" type="checkbox" [% IF ( index.sortable ) %]checked="checked"[% END %] value="[% index.code %]" />
</td>
<td>
<input name="facetable" type="checkbox" [% IF ( index.facetable ) %]checked="checked"[% END %] value="[% index.code %]" />
</td>
<td>
<textarea name="mappings" class="contentEditable">[% FOREACH m IN index.mappings %][% m %]
[% END %]</textarea> <!-- Don't indent this line -->
</td>
<td>[% UNLESS ( index.mandatory ) %]<a class="delete">Delete</a>[% END %]</td>
</tr>
[% END %]
</tbody>
<tfoot>
<tr class="nodrag nodrop">
<td>
<input data-id="code" type="text" maxlength="25" />
<input data-id="ressource_type" type="hidden" value="[% rt.ressource_type %]" />
<input data-id="mandatory" type="hidden" value="0" />
</td>
<td><input data-id="label" type="text" maxlength="25" /></td>
<td>
<select data-id="type">
<option value="str">String</option>
<option value="ste">Simple Text</option>
<option value="txt">Text</option>
<option value="int">Integer</option>
<option value="date">Date</option>
</select>
</td>
<td><input data-id="sortable" type="checkbox" /></td>
<td><input data-id="facetable" type="checkbox" /></td>
<td>
<textarea data-id="mappings" class="contentEditable"></textarea>
</td>
<td><a class="add">Add</a></td>
</tr>
</tfoot>
</table>
[% END %]
<p><input type="submit" value="Save" /></p>
</form>
</div>
</div>
<div class="yui-b">
[% INCLUDE 'admin-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -0,0 +1,56 @@
[% IF facets_loop %]
<div id="search-facets">
<h4>Refine your search</h4>
<ul>
[% FOR facets IN facets_loop %]
<li>
[% facets.label %]
<ul class="facets_list">
[% FOR value IN facets.values %]
<li>
[% IF ( value.active ) %]
[% value.lib %] ([% value.count %])
[% SET url = "/cgi-bin/koha/opac-search.pl?" %]
[% SET first = 1 %]
[% FOR p IN follower_params %]
[% IF p.var != 'filters' %]
[% UNLESS first %]
[% SET url = url _ '&' %]
[% END %]
[% SET first = 0 %]
[% SET url = url _ p.var _ '=' _ p.val %]
[% END %]
[% END %]
[% FOR f IN filters %]
[% UNLESS f.var == facets.indexname && f.val == value.value %]
[% SET url = url _ '&filters=' _ f.var _ ':&quot;' _ f.val _ '&quot;' %]
[% END %]
[% END %]
[<a href="[% url |url %]">x</a>]
[% ELSE %]
[% SET url = "/cgi-bin/koha/opac-search.pl?" %]
[% SET first = 1 %]
[% FOR p IN follower_params %]
[% IF p.var != 'filters' %]
[% UNLESS first %]
[% SET url = url _ '&' %]
[% END %]
[% SET first = 0 %]
[% SET url = url _ p.var _ '=' _ p.val %]
[% END %]
[% END %]
[% FOR f IN filters %]
[% SET url = url _ '&filters=' _ f.var _ ':&quot;' _ f.val _ '&quot;' %]
[% END %]
[% SET url = url _ '&filters=' _ facets.indexname _ ':&quot;' _ value.value _ '&quot;' %]
<a href="[% url |url %]">[% value.lib %]</a> ([% value.count %])
[% END %]
</li>
[% END %]
</ul>
</li>
[% END %]
</ul>
</div>
[% END %]

View File

@ -0,0 +1,17 @@
[% IF ( PAGE_NUMBERS ) %]
<div class="pages">
[% IF ( previous_page ) %]
<a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&amp;[% END %]page=[% previous_page |url %]">&lt;&lt; Previous</a>
[% END %]
[% FOREACH PAGE_NUMBER IN PAGE_NUMBERS %]
[% IF ( PAGE_NUMBER.current ) %]
<span class="current">[% PAGE_NUMBER.page %]</span>
[% ELSE %]
<a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&amp;[% END %]page=[% PAGE_NUMBER.page |url %]">[% PAGE_NUMBER.page %]</a>
[% END %]
[% END %]
[% IF ( next_page ) %]
<a class="nav" href="?[% FOREACH fp IN follower_params %][% fp.var |url %]=[% fp.val |url %]&amp;[% END %]page=[% next_page |url %]">Next &gt;&gt;</a>
[% END %]
</div>
[% END %]

View File

@ -0,0 +1,23 @@
[% IF sort_by == "score asc" %]
<option value="score asc" selected="selected">Relevance asc</option>
[% ELSE %]
<option value="score asc">Relevance asc</option>
[% END %]
[% IF sort_by == "score desc" %]
<option value="score desc" selected="selected">Relevance desc</option>
[% ELSE %]
<option value="score desc">Relevance desc</option>
[% END %]
[% FOREACH ind IN sortable_indexes %]
[% IF sort_by == "$ind.code asc" %]
<option value="[% ind.code %] asc" selected="selected">[% ind.label %] asc</option>
[% ELSE %]
<option value="[% ind.code %] asc">[% ind.label %] asc</option>
[% END %]
[% IF sort_by == "$ind.code desc" %]
<option value="[% ind.code %] desc" selected="selected">[% ind.label %] desc</option>
[% ELSE %]
<option value="[% ind.code %] desc">[% ind.label %] desc</option>
[% END %]
[% END %]

View File

@ -0,0 +1,108 @@
[% INCLUDE 'doc-head-open.inc' %]
[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo;
[% IF ( searchdesc ) %]
Results of search [% IF ( query_desc ) %]for '[% query_desc | html%]'[% END %][% IF ( limit_desc ) %]&nbsp;with limit(s):&nbsp;'[% limit_desc | html %]'[% END %]
[% ELSE %]
You did not specify any search criteria.
[% END %]
[% INCLUDE 'doc-head-close.inc' %]
<link rel="alternate" type="application/rss+xml" title="[% LibraryName |html %] Search RSS Feed" href="[% OPACBaseurl %]/cgi-bin/koha/opac-search.pl?[% query_cgi |html %][% limit_cgi |html %]&amp;count=[% countrss |html %]&amp;sort_by=acqdate_dsc&amp;format=rss2" />
<script type="text/javascript" src="/opac-tmpl/prog/en/lib/jquery/jquery.js"></script>
<link rel="stylesheet" type="text/css" href="/opac-tmpl/prog/en/css/jquery.rating.css" />
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#bookbag_form').find("input").hide();
$('#sort_by').change(function() {
$('#bookbag_form').submit();
});
} );
</script>
</head>
<body id="results">
<div id="doc3" class="yui-t1">
<div id="bd">
[% INCLUDE 'masthead.inc' %]
<div id="yui-main">
<div class="yui-b">
<div id="userresults" class="container">
[% IF ( query_error ) %]
<div class="dialog alert">
<h4>Error:</h4>
[% query_error %]
</div>
[% END %]
<!-- Search Results Table -->
[% IF ( total ) %]
<div class="num_of_results">
We have [% total %] results for your search
</div>
<div class="searchresults">
<form action="/cgi-bin/koha/opac-search.pl" method="get" name="bookbag_form" id="bookbag_form">
<!-- TABLE RESULTS START -->
<table>
<thead>
<tr>
<th colspan="5" class="resultscontrol">
<div class="resort">
<form method="get" id="sortbyform">
[% FOREACH param IN follower_params %]
[% UNLESS param.var == 'sort_by' %]
<input type="hidden" name='[% param.var |html %]' value='[% param.val %]' />
[% END %]
[% END %]
<label for="sort_by">Sort By: </label>
<select id="sort_by" name="sort_by">
[% INCLUDE 'search/resort_form.inc' %]
</select>
<input type="submit" value="Go" />
</form>
</div>
<div class="cartlist">
<!-- checkall, clearall are now needed for placehold -->
<span class="checkall"></span>
<span class="clearall"></span>
</div>
</th>
</tr>
</thead>
<!-- Actual Search Results -->
<tbody>
[% FOREACH SEARCH_RESULT IN SEARCH_RESULTS %]
<tr>
<td>
<input type="checkbox" id="bib[% SEARCH_RESULT.biblionumber %]" name="biblionumber" value="[% SEARCH_RESULT.biblionumber %]" /> <label for="bib[% SEARCH_RESULT.biblionumber %]"></label>
</td>
<td>
<a class="title" href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% SEARCH_RESULT.biblionumber |url %]" title="View details for this title">[% SEARCH_RESULT.title |html %]</a>
by <a href="/cgi-bin/koha/opac-search.pl?q=author:[% SEARCH_RESULT.author |url %]" title="Search for works by this author" class="author">[% SEARCH_RESULT.author %]</a>
</td>
</tr>
[% END %]
</tbody>
</table>
</form>
</div>
[% INCLUDE 'search/page-numbers.inc' %]
[% END %]
</div>
</div>
</div>
<div class="yui-b">
<div class="container">
[% INCLUDE 'search/facets.inc' %]
</div>
</div>
</div>
[% INCLUDE 'opac-bottom.inc' %]

View File

@ -16,7 +16,7 @@ the kohaversion is divided in 4 parts :
use strict;
sub kohaversion {
our $VERSION = '3.09.00.022';
our $VERSION = '3.09.00.023';
# version needs to be set this way
# so that it can be picked up by Makefile.PL
# during install

View File

@ -0,0 +1,179 @@
#!/usr/bin/perl
# Copyright 2012 BibLibre SARL
#
# 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 2 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 Data::Dumper;
use Getopt::Long;
use LWP::Simple;
use XML::Simple;
use C4::Context;
use C4::Search;
use Koha::SearchEngine::Index;
$|=1; # flushes output
if ( C4::Context->preference("SearchEngine") ne 'Solr' ) {
warn "System preference 'SearchEngine' not equal 'Solr'.";
warn "We can not indexing";
exit(1);
}
#Setup
my ( $reset, $number, $recordtype, $biblionumbers, $optimize, $info, $want_help );
GetOptions(
'r' => \$reset,
'n:s' => \$number,
't:s' => \$recordtype,
'w:s' => \$biblionumbers,
'o' => \$optimize,
'i' => \$info,
'h|help' => \$want_help,
);
my $debug = C4::Context->preference("DebugLevel");
my $index_service = Koha::SearchEngine::Index->new;
my $solrurl = $index_service->searchengine->config->SolrAPI;
my $ping = &ping_command;
if (!defined $ping) {
print "SolrAPI = $solrurl\n";
print "Solr is Down\n";
exit(1);
}
#Script
&print_help if ($want_help);
&print_info if ($info);
if ($reset){
if ($recordtype){
&reset_index("recordtype:".$recordtype);
} else {
&reset_index("*:*");
}
}
if (defined $biblionumbers){
if (not defined $recordtype) { print "You must specify a recordtype\n"; exit 1;}
&index_biblio($_) for split ',', $biblionumbers;
} elsif (defined $recordtype) {
&index_data;
&optimise_index;
}
if ($optimize) {
&optimise_index;
}
#Functions
sub index_biblio {
my ($biblionumber) = @_;
$index_service->index_record($recordtype, [ $biblionumber ] );
}
sub index_data {
my $dbh = C4::Context->dbh;
$dbh->do('SET NAMES UTF8;');
my $query;
if ( $recordtype eq 'biblio' ) {
$query = "SELECT biblionumber FROM biblio ORDER BY biblionumber";
} elsif ( $recordtype eq 'authority' ) {
$query = "SELECT authid FROM auth_header ORDER BY authid";
}
$query .= " LIMIT $number" if $number;
my $sth = $dbh->prepare( $query );
$sth->execute();
$index_service->index_record($recordtype, [ map { $_->[0] } @{ $sth->fetchall_arrayref } ] );
$sth->finish;
}
sub reset_index {
&reset_command;
&commit_command;
$debug eq '2' && &count_all_docs eq 0 && warn "Index cleaned!"
}
sub commit_command {
my $commiturl = "/update?stream.body=%3Ccommit/%3E";
my $urlreturns = get $solrurl.$commiturl;
}
sub ping_command {
my $pingurl = "/admin/ping";
my $urlreturns = get $solrurl.$pingurl;
}
sub reset_command {
my ($query) = @_;
my $deleteurl = "/update?stream.body=%3Cdelete%3E%3Cquery%3E".$query."%3C/query%3E%3C/delete%3E";
my $urlreturns = get $solrurl.$deleteurl;
}
sub optimise_index {
$index_service->optimize;
}
sub count_all_docs {
my $queryurl = "/select/?q=*:*";
my $urlreturns = get $solrurl.$queryurl;
my $xmlsimple = XML::Simple->new();
my $data = $xmlsimple->XMLin($urlreturns);
return $data->{result}->{numFound};
}
sub print_info {
my $count = &count_all_docs;
print <<_USAGE_;
SolrAPI = $solrurl
How many indexed documents = $count;
_USAGE_
}
sub print_help {
print <<_USAGE_;
$0: reindex biblios and/or authorities in Solr.
Use this batch job to reindex all biblio or authority records in your Koha database. This job is useful only if you are using Solr search engine.
Parameters:
-t biblio index bibliographic records
-t authority index authority records
-r clear Solr index before adding records to index - use this option carefully!
-n 100 index 100 first records
-n "100,2" index 2 records after 100th (101 and 102)
-w 101 index biblio with biblionumber equals 101
-o launch optimize command at the end of indexing
-i gives solr install information: SolrAPI value and count all documents indexed
--help or -h show this message.
_USAGE_
}

View File

@ -21,14 +21,26 @@
# Script to perform searching
# Mostly copied from search.pl, see POD there
use strict; # always use
use warnings;
use Modern::Perl;
## STEP 1. Load things that are used in both search page and
# results page and decide which template to load, operations
# to perform, etc.
## load Koha modules
use C4::Context;
my $searchengine = C4::Context->preference("SearchEngine");
given ( $searchengine ) {
when ( /^Solr$/ ) {
warn "We use Solr";
require 'opac/search.pl';
exit;
}
when ( /^Zebra$/ ) {
}
}
use C4::Output;
use C4::Auth qw(:DEFAULT get_session);
use C4::Languages qw(getAllLanguages);

172
opac/search.pl Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/perl
# Copyright 2012 BibLibre
#
# 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 2 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 C4::Context;
use CGI;
use C4::Auth;
use C4::Koha;
use C4::Output;
use Koha::SearchEngine::Search;
use Koha::SearchEngine::QueryBuilder;
use Koha::SearchEngine::FacetsBuilder;
my $cgi = new CGI;
my $template_name;
my $template_type = "basic";
if ( $cgi->param("idx") or $cgi->param("q") ) {
$template_name = 'search/results.tt';
} else {
$template_name = 'search/advsearch.tt';
$template_type = 'advsearch';
}
# load the template
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
{ template_name => $template_name,
query => $cgi,
type => "opac",
authnotrequired => 1,
}
);
my $format = $cgi->param("format") || 'html';
# load the Type stuff
my $itemtypes = GetItemTypes;
my $page = $cgi->param("page") || 1;
my $count = $cgi->param('count') || C4::Context->preference('OPACnumSearchResults') || 20;
$count = 5;
my $q = $cgi->param("q");
my $builder = Koha::SearchEngine::QueryBuilder->new;
$q = $builder->build_query( $q );
my $search_service = Koha::SearchEngine::Search->new;
# load the sorting stuff
my $sort_by = $cgi->param('sort_by')
|| C4::Context->preference('OPACdefaultSortField') . ' ' . C4::Context->preference('OPACdefaultSortOrder');
my $search_engine_config = Koha::SearchEngine->new->config;
my $sortable_indexes = $search_engine_config->sortable_indexes;
my ( $sort_indexname, $sort_order );
( $sort_indexname, $sort_order ) = ($1, $2) if ( $sort_by =~ m/^(.*) (asc|desc)$/ );
my $sort_by_indexname = eval {
[
map {
$_->{code} eq $sort_indexname
? 'srt_' . $_->{type} . '_' . $_->{code} . ' ' . $sort_order
: ()
} @$sortable_indexes
]->[0]
};
# This array is used to build facets GUI
my %filters;
my @tplfilters;
for my $filter ( $cgi->param('filters') ) {
next if not $filter;
my ($k, @v) = $filter =~ /(?: \\. | [^:] )+/xg;
my $v = join ':', @v;
push @{$filters{$k}}, $v;
$v =~ s/^"(.*)"$/$1/; # Remove quotes around
push @tplfilters, {
'var' => $k,
'val' => $v,
};
}
push @{$filters{recordtype}}, 'biblio';
my $results = $search_service->search(
$q,
\%filters,
{
page => $page,
count => $count,
sort => $sort_by_indexname,
facets => 1,
fl => ["ste_title", "str_author", 'int_biblionumber'],
}
);
if ($results->{error}){
$template->param(query_error => $results->{error});
output_with_http_headers $cgi, $cookie, $template->output, 'html';
exit;
}
# populate results with records
my @r;
for my $searchresult ( @{ $results->items } ) {
my $biblionumber = $searchresult->{values}->{recordid};
my $nr;
while ( my ($k, $v) = each %{$searchresult->{values}} ) {
my $nk = $k;
$nk =~ s/^[^_]*_(.*)$/$1/;
$nr->{$nk} = ref $v ? shift @{$v} : $v;
}
push( @r, $nr );
}
# build facets
my $facets_builder = Koha::SearchEngine::FacetsBuilder->new;
my @facets_loop = $facets_builder->build_facets( $results, $search_engine_config->facetable_indexes, \%filters );
my $total = $results->{pager}->{total_entries};
my $pager = Data::Pagination->new(
$total,
$count,
20,
$page,
);
# params we want to pass for all actions require another query (pagination, sort, facets)
my @follower_params = map { {
var => 'filters',
val => $_->{var}.':"'.$_->{val}.'"'
} } @tplfilters;
push @follower_params, { var => 'q', val => $q};
push @follower_params, { var => 'sort_by', val => $sort_by};
# Pager template params
$template->param(
previous_page => $pager->{'prev_page'},
next_page => $pager->{'next_page'},
PAGE_NUMBERS => [ map { { page => $_, current => $_ == $page } } @{ $pager->{'numbers_of_set'} } ],
current_page => $page,
follower_params => \@follower_params,
total => $total,
SEARCH_RESULTS => \@r,
query => $q,
count => $count,
sort_by => $sort_by,
sortable_indexes => $sortable_indexes,
facets_loop => \@facets_loop,
filters => \@tplfilters,
);
my $content_type = ( $format eq 'rss' or $format eq 'atom' ) ? $format : 'html';
output_with_http_headers $cgi, $cookie, $template->output, $content_type;

23
t/lib/Mocks.pm Normal file
View File

@ -0,0 +1,23 @@
package t::lib::Mocks;
use Modern::Perl;
use Test::MockModule;
use t::lib::Mocks::Context;
our (@ISA,@EXPORT,@EXPORT_OK);
BEGIN {
require Exporter;
@ISA = qw(Exporter);
push @EXPORT, qw(
&set_solr
&set_zebra
);
}
my $context = new Test::MockModule('C4::Context');
sub set_solr {
$context->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, "Solr", $context ) });
}
sub set_zebra {
$context->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, "Zebra", $context ) });
}

13
t/lib/Mocks/Context.pm Normal file
View File

@ -0,0 +1,13 @@
package t::lib::Mocks::Context;
use t::lib::Mocks::Context;
use C4::Context;
sub MockPreference {
my ( $self, $syspref, $value, $mock_object ) = @_;
return $value if $syspref eq 'SearchEngine';
$mock_object->unmock("preference");
my $r = C4::Context->preference($syspref);
$mock_object->mock('preference', sub { &t::lib::Mocks::Context::MockPreference( @_, $value, $mock_object ) });
return $r;
}
1;

View File

@ -0,0 +1,23 @@
use Modern::Perl;
use Test::More;
use Koha::SearchEngine::Solr;
use Koha::SearchEngine::Zebra;
use Koha::SearchEngine::Search;
use t::lib::Mocks;
my $se_index = Koha::SearchEngine::Solr->new;
ok($se_index->isa('Data::SearchEngine::Solr'), 'Solr is a Solr data searchengine');
$se_index = Koha::SearchEngine::Zebra->new;
ok($se_index->isa('Data::SearchEngine::Zebra'), 'Zebra search engine');
set_solr();
$se_index = Koha::SearchEngine::Search->new;
ok($se_index->searchengine->isa('Data::SearchEngine::Solr'), 'Solr search engine');
set_zebra();
$se_index = Koha::SearchEngine::Search->new;
ok($se_index->searchengine->isa('Data::SearchEngine::Zebra'), 'Zebra search engine');
done_testing;

View File

@ -0,0 +1,12 @@
use Test::More;
use t::lib::Mocks;
set_solr;
use Koha::SearchEngine::Search;
my $search_service = Koha::SearchEngine::Search->new;
isnt (scalar $search_service->search("fort"), 0, 'test search') ;
#$search_service->search($query_service->build_query(@,@,@));
done_testing;

View File

@ -0,0 +1,15 @@
use Test::More;
use FindBin qw($Bin);
use t::lib::::Mocks;
use Koha::SearchEngine::Index;
set_solr;
my $index_service = Koha::SearchEngine::Index->new;
system( qq{/bin/cp $FindBin::Bin/../indexes.yaml /tmp/indexes.yaml} );
$index_service->searchengine->config->set_config_filename( "/tmp/indexes.yaml" );
is ($index_service->index_record("biblio", [2]), 1, 'test search') ;
is ($index_service->optimize, 1, 'test search') ;
done_testing;

View File

@ -0,0 +1,45 @@
use Modern::Perl;
use Test::More;
use C4::Context;
use Koha::SearchEngine;
use Koha::SearchEngine::QueryBuilder;
use t::lib::Mocks;
my $titleindex = "title";
my $authorindex = "author";
#my $eanindex = "str_ean";
#my $pubdateindex = "date_pubdate";
my ($operands, $indexes, $operators);
# === Solr part ===
@$operands = ('cup', 'rowling');
@$indexes = ('ti', 'au');
@$operators = ('AND');
set_solr;
my $qs = Koha::SearchEngine::QueryBuilder->new;
my $se = Koha::SearchEngine->new;
is( $se->name, "Solr", "Test searchengine name eq Solr" );
my $gotsolr = $qs->build_advanced_query($indexes, $operands, $operators);
my $expectedsolr = "ti:cup AND au:rowling";
is($gotsolr, $expectedsolr, "Test build_query Solr");
# === Zebra part ===
set_zebra;
$se = Koha::SearchEngine->new;
is( $se->name, "Zebra", "Test searchengine name eq Zebra" );
$qs = Koha::SearchEngine::QueryBuilder->new;
my ( $builterror, $builtquery, $simple_query, $query_cgi, $query_desc, $limit, $limit_cgi, $limit_desc, $stopwords_removed, $query_type ) = $qs->build_query($operators, $operands, $indexes);
my $gotzebra = $builtquery;
my $expectedzebra = qq{ti,wrdl= cup AND au,wrdl= rowling };
is($gotzebra, $expectedzebra, "Test Zebra indexes in 'normal' search");
# @and @attr 1=title @attr 4=6 "des mots de mon titre" @attr 1=author Jean en PQF
done_testing;

View File

@ -0,0 +1,54 @@
use Modern::Perl;
use Test::More;
use FindBin qw($Bin);
use C4::Context;
use Koha::SearchEngine;
use t::lib::Mocks;
set_solr;
my $se = Koha::SearchEngine->new;
is( $se->name, "Solr", "Test searchengine name eq Solr" );
my $config = $se->config;
$config->set_config_filename( "$FindBin::Bin/../indexes.yaml" );
my $ressource_types = $config->ressource_types;
is ( grep ( /^biblio$/, @$ressource_types ), 1, "Ressource type biblio must to be defined" );
is ( grep ( /^authority$/, @$ressource_types ), 1, "Ressource type authority must to be defined" );
my $indexes = $config->indexes;
is ( scalar(@$indexes), 3, "There are 3 indexes configured" );
my $index1 = @$indexes[0];
is ( $index1->{code}, 'title', "My index first have code=title");
is ( $index1->{label}, 'Title', "My index first have label=Title");
is ( $index1->{type}, 'ste', "My index first have type=ste");
is ( $index1->{ressource_type}, 'biblio', "My index first have ressource_type=biblio");
is ( $index1->{sortable}, '1', "My index first have sortable=1");
is ( $index1->{mandatory}, '1', "My index first have mandatory=1");
eq_array ( $index1->{mappings}, ["200\$a", "4..\$t"], "My first index have mappings=[200\$a,4..\$t]");
system( qq{/bin/cp $FindBin::Bin/../indexes.yaml /tmp/indexes.yaml} );
$config->set_config_filename( "/tmp/indexes.yaml" );
$indexes = $config->indexes;
my $new_index = {
code => 'isbn',
label => 'ISBN',
type => 'str',
ressource_type => 'biblio',
sortable => 0,
mandatory => 0
};
push @$indexes, $new_index;
$config->indexes( $indexes );
$indexes = $config->indexes;
my $isbn_index = $config->index( 'isbn' );
is( $isbn_index->{code}, 'isbn', 'Index isbn has been written' );
my $sortable_indexes = $config->sortable_indexes;
is ( @$sortable_indexes, 2, "There are 2 sortable indexes" );
done_testing;

View File

@ -0,0 +1,33 @@
ressource_types:
- biblio
- authority
indexes:
- code: title
label: Title
type: ste
ressource_type: biblio
sortable: 1
mandatory: 1
mappings:
- 200$a
- 210$a
- 4..$t
- code: author
label: Author
type: str
ressource_type: biblio
sortable: 1
mandatory: 0
mappings:
- 700$*
- 710$*
- code: subject
label: Subject
type: str
ressource_type: biblio
sortable: 0
mandatory: 0
mappings:
- 600$a
- 601$a