How do I dynamically build a search block in sunspot? - sunspot

I am converting a Rails app from using acts_as_solr to sunspot.
The app uses the field search capability in solr that was exposed in acts_as_solr. You could give it a query string like this:
title:"The thing to search"
and it would search for that string in the title field.
In converting to sunspot I am parsing out field specific portions of the query string and I need to dynamically generate the search block. Something like this:
Sunspot.search(table_clazz) do
keywords(first_string, :fields => :title)
keywords(second_string, :fields => :description)
...
paginate(:page => page, :per_page => per_page)
end
This is complicated by also needing to do duration (seconds, integer) ranges and negation if the query requires it.
On the current system users can search for something in the title, excluding records with something else in another field and scoping by duration.
In a nutshell, how do I generate these blocks dynamically?

I recently did this kind of thing using instance_eval to evaluate procs (created elsewhere) in the context of the Sunspot search block.
The advantage is that these procs can be created anywhere in your application yet you can write them with the same syntax as if you were inside a sunspot search block.
Here's a quick example to get you started for your particular case:
def build_sunspot_query(conditions)
condition_procs = conditions.map{|c| build_condition c}
Sunspot.search(table_clazz) do
condition_procs.each{|c| instance_eval &c}
paginate(:page => page, :per_page => per_page)
end
end
def build_condition(condition)
Proc.new do
# write this code as if it was inside the sunspot search block
keywords condition['words'], :fields => condition[:field].to_sym
end
end
conditions = [{words: "tasty pizza", field: "title"},
{words: "cheap", field: "description"}]
build_sunspot_query conditions
By the way, if you need to, you can even instance_eval a proc inside of another proc (in my case I composed arbitrarily-nested 'and'/'or' conditions).

Sunspot provides a method called Sunspot.new_search which lets you build the search conditions incrementally and execute it on demand.
An example provided by the Sunspot's source code:
search = Sunspot.new_search do
with(:blog_id, 1)
end
search.build do
keywords('some keywords')
end
search.build do
order_by(:published_at, :desc)
end
search.execute
# This is equivalent to:
Sunspot.search do
with(:blog_id, 1)
keywords('some keywords')
order_by(:published_at, :desc)
end
With this flexibility, you should be able to build your query dynamically. Also, you can extract common conditions to a method, like so:
def blog_facets
lambda { |s|
s.facet(:published_year)
s.facet(:author)
}
end
search = Sunspot.new_search(Blog)
search.build(&blog_facets)
search.execute

I have solved this myself. The solution I used was to compiled the required scopes as strings, concatenate them, and then eval them inside the search block.
This required a separate query builder library that interrogates the solr indexes to ensure that a scope is not created for a non existent index field.
The code is very specific to my project, and too long to post in full, but this is what I do:
1. Split the search terms
this gives me an array of the terms or terms plus fields:
['field:term', 'non field terms']
2. This is passed to the query builder.
The builder converts the array to scopes, based on what indexes are available. This method is an example that takes the model class, field and value and returns the scope if the field is indexed.
def convert_text_query_to_search_scope(model_clazz, field, value)
if field_is_indexed?(model_clazz, field)
escaped_value = value.gsub(/'/, "\\\\'")
"keywords('#{escaped_value}', :fields => [:#{field}])"
else
""
end
end
3. Join all the scopes
The generated scopes are joined join("\n") and that is evaled.
This approach allows the user to selected the models they want to search, and optionally to do field specific searching. The system will then only search the models with any specified fields (or common fields), ignoring the rest.
The method to check if the field is indexed is:
# based on http://blog.locomotivellc.com/post/6321969631/sunspot-introspection
def field_is_indexed?(model_clazz, field)
# first part returns an array of all indexed fields - text and other types - plus ':class'
Sunspot::Setup.for(model_clazz).all_field_factories.map(&:name).include?(field.to_sym)
end
And if anyone needs it, a check for sortability:
def field_is_sortable?(classes_to_check, field)
if field.present?
classes_to_check.each do |table_clazz|
return false if ! Sunspot::Setup.for(table_clazz).field_factories.map(&:name).include?(field.to_sym)
end
return true
end
false
end

Related

How to isolate a list of URIs in a RDF file using CONSTRUCT or DESCRIBE in SPARQL?

I'm trying to get only a list of URIs in RDF instead of a list of triples:
PREFIX gr: <http://purl.org/goodrelations/v1#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
DESCRIBE ?product
WHERE
{
?product rdfs:subClassOf gr:ProductOrService .
}
Using SELECT, instead of DESCRIBE I receive only the subject (which I want), but not as an RDF but like a SPARQL Result with binds, vars, etc.
Using CONSTRUCT, I can't specify only the ?product, as above, so the closest I can get is:
PREFIX gr: <http://purl.org/goodrelations/v1#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
CONSTRUCT
WHERE
{
?product rdfs:subClassOf gr:ProductOrService .
}
Which returns an RDF with triples of different products, but the same properties and objects.
It looks like you should read over the SPARQL specification, which tells you that this is exactly as expected. To get the single column you wish, you must use SELECT.
SPARQL has four query forms. These query forms use the solutions from pattern matching to form result sets or RDF graphs. The query forms are:
SELECT
Returns all, or a subset of, the variables bound in a query pattern match.
CONSTRUCT
Returns an RDF graph constructed by substituting variables in a set of triple templates.
ASK
Returns a boolean indicating whether a query pattern matches or not.
DESCRIBE
Returns an RDF graph that describes the resources found.
I found a solution. I've used SELECT, but instead of binds and vars, as output received a "Comma-Separated Values (with fields in N-Triples syntax)" CSV file:
<http://www.productontology.org/id/Real_estate>
<http://www.productontology.org/id/Automobile>
<http://www.productontology.org/id/Auction>
<http://www.productontology.org/id/Video_game>
<http://www.productontology.org/id/Campsite>
<http://www.productontology.org/id/Car>
<http://www.productontology.org/id/Audiobook>
<http://www.productontology.org/id/Browser_game>
...
Representing the result of a SPARQL SELECT query as an RDF List is a tooling issue - it is not something that can be solved in SPARQL in general.
Some SPARQL tools may have ways to support rendering the query result as an RDF list. But it's not something you can fix by just formulating your query differently, you'll need to use a tool to (programmatically) format the result.
In Java, using Eclipse RDF4J, you can do it as follows (untested so you may need to tweak it to work properly, but it should give you a general idea):
String query = "SELECT ?product WHERE { ?product rdfs:subClassOf gr:ProductOrService . }";
// do the query on repo and convert to a Java list of URI objects
List<URI> results = Repositories.tupleQuery(
repo,
query,
r -> QueryResults.stream(r).map(bs -> (URI)bs.getValue("product")).collect(Collectors.toList()
);
// create a resource (bnode or URI) for the start of the rdf:List
Resource head = SimpleValueFactory.getInstance().createBNode();
// convert the Java list of results to an rdf:list and add it
// to a newly-created RDF Model
Model m = RDFCollections.asRDF(results, head, new LinkedHashModel());
Once you have your result as an RDF model you can use any of the existing RDF4J APIs to write it to file or to store it in a triplestore.
Now, I am not claiming that any of this is a good idea - I have never seen a use case for something like this. But this is how I would do it if it were necessary.

Sensenet length filter Not working

I want to query the Field which are empty and which are not empty using Sensenet Odata Rest API. Their documentation mentions a filter function called 'length'. I have tried to query the field with the length operation but it fails with the error.
This is the filter I have used
$filter=length(Name) eq 2
Sense/Net 6.5.4.9496
Exception
"code": "NotSpecified",
"exceptiontype": "SnNotSupportedException",
"message": {
"lang": "en-us",
"value": "Unknown method: length"
},
Wiki Link http://wiki.sensenet.com/OData_REST_API
The length operation was included in the list of supported methods incorrectly, we apologise for that. SenseNet compiles these filters to Lucene queries and it is not possible to compose such a query in Lucene that performs an operation on a field.
(the remaining methods, like substringof or startswith can be compiled to a wildcard expression easily, so that should work)
Unfortunately 'empty' expressions are also not supported by Lucene, because of their document/term structure. So the following expression does not work either:
Description eq ''
Edit: as a workaround, developers may create a custom field index handler.
For every field you want to check for emptiness (e.g. Description), you may create a technical hidden bool field (IsDescriptionEmpty) in the content type definition. The only thing you have to create and define is a custom field index handler class. In your case it would inherit from the built-in bool field index handler and you could return a boolean index value based on whether the target field (in this case Description) is empty or not.
After this you would be able to define search exressions like the following:
+Type:File +IsDescriptionEmpty:true
Please check the wiki article below and the source code for index handler examples.
How to create a field indexhandler

Erlang mnesia equivalent of "select * from Tb"

I'm a total erlang noob and I just want to see what's in a particular table I have. I want to just "select *" from a particular table to start with. The examples I'm seeing, such as the official documentation, all have column restrictions which I don't really want. I don't really know how to form the MatchHead or Guard to match anything (aka "*").
A very simple primer on how to just get everything out of a table would be very appreciated!
For example, you can use qlc:
F = fun() ->
Q = qlc:q([R || R <- mnesia:table(foo)]),
qlc:e(Q)
end,
mnesia:transaction(F).
The simplest way to do it is probably mnesia:dirty_match_object:
mnesia:dirty_match_object(foo, #foo{_ = '_'}).
That is, match everything in the table foo that is a foo record, regardless of the values of the fields (every field is '_', i.e. wildcard). Note that since it uses record construction syntax, it will only work in a module where you have included the record definition, or in the shell after evaluating rr(my_module) to make the record definition available.
(I expected mnesia:dirty_match_object(foo, '_') to work, but that fails with a bad_type error.)
To do it with select, call it like this:
mnesia:dirty_select(foo, [{'_', [], ['$_']}]).
Here, MatchHead is _, i.e. match anything. The guards are [], an empty list, i.e. no extra limitations. The result spec is ['$_'], i.e. return the entire record. For more information about match specs, see the match specifications chapter of the ERTS user guide.
If an expression is too deep and gets printed with ... in the shell, you can ask the shell to print the entire thing by evaluating rp(EXPRESSION). EXPRESSION can either be the function call once again, or v(-1) for the value returned by the previous expression, or v(42) for the value returned by the expression preceded by the shell prompt 42>.

Though I have a record in database, it's giving "Mongoid::Errors::DocumentNotFound"

Though I have the record with id 13163 (db.locations.find({_id: 13163})), it's giving me error:
Mongoid::Errors::DocumentNotFound in LocationsController#show
Problem: Document(s) not found for class Location with id(s) 13163.
Summary: When calling Location.find with an id or array of ids, each
parameter must match a document in the database or this error will be
raised. The search was for the id(s): 13163 ... (1 total) and the
following ids were not found: 13163. Resolution: Search for an id that
is in the database or set the Mongoid.raise_not_found_error
configuration option to false, which will cause a nil to be returned
instead of raising this error when searching for a single id, or only
the matched documents when searching for multiples.
# Use callbacks to share common setup or constraints between actions.
def set_location
#location = Location.find(params[:id])
end
locations_controller.rb:
class LocationsController < ApplicationController
before_action :set_location, only: [:show, :edit, :update, :destroy]
# GET /locations
# GET /locations.json
def index
#locations = Location.all
end
# GET /locations/1
# GET /locations/1.json
def show
end
private
# Use callbacks to share common setup or constraints between actions.
def set_location
#location = Location.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def location_params
params.require(:location).permit(:loc_name_en, :loc_name_jp, :channel)
end
end
Setting up the option raise_not_found_error: false is not the case as I do have a document in database.
SOLUTION:
Big thanks to #mu is too short for giving me a hint.
The problem can be solved in 2 ways:
Declare field :_id, type: Integer in the model location.rb
Or converting the passing parameter to Integer like Location.find(params[:id].to_i) in locations_controller.rb as shown below in the #mu is too short's answer
I'd guess that you have a type problem. You say that this:
db.locations.find({_id: 13163})
finds the document in the MongoDB shell. That means that you have a document in the locations collection whose _id is the number 13163. If you used the string '13163':
db.locations.find({_id: '13163'})
you won't find your document. The value in params[:id] is probably a string so you're saying:
Location.find('13163')
when you want to say:
Location.find(13163)
If the _id really is a number then you'll need to make sure you call find with a number:
Location.find(params[:id].to_i)
You're probably being confused because sometimes Mongoid will convert between Strings and Moped::BSON::ObjectIds (and sometimes it won't) so if your _id is the usual ObjectId you can say:
Model.find('5016cd8b30f1b95cb300004d')
and Mongoid will convert that string to an ObjectId for you. Mongoid won't convert a String to a number for you, you have to do that yourself.

Query criteria: from public variable or from form

After a few hours searching, I couldn't find any solution to this little problem I'm having.
I have a query that retrieves one of its criteria from a form. I have referenced correctly the value on the form from the query, and it works, but what I wanted to do is a bit more complicated: when the form is closed, I want to launch the query with a "default value".
I tried to do it in 2 different ways:
a) Defining an "IIf" at the query criteria: I would need a function that checks if the form from which I retrieve the values is open.
b) Defining public variables with a default value, which would be changed from the form: I don't know where/when to initialize the value of the variable.
Does anyone have a better idea on how to do this?
TL;DR: Query gets criteria from form when it's open. If form is closed, query uses default value. HELP!
You can create a VBA function in a module to do this :
Function MyCriterion() As Long
MyCriterion = 1234 ' default value
If CurrentProject.AllForms("MyForm").IsLoaded Then
MyCriterion = Forms("MyForm").MyControl.Value
End If
End Function