Search for empty string field in Sunspot (Solr) - ruby-on-rails

In my model, I'm simply using something like:
class Person < ActiveRecord::Base
searchable do
string :middle_name
end
end
The particular object I'm trying to search for has a :middle_name attribute that contains '', an empty string and is of the String datatype. Based on that information, I am assuming that Sunspot is also saving an empty string for that field in the Solr index.
After successfully doing Person.reindex and Sunspot.commit, I tried searching for the said object using Person.search{with(:middle_name, '')}.resultsin the rails console and it returns a 400 error in regards to Solr query syntax.
I then looked around and found some information on a query like Person.search{with(:middle_name, "* TO ''")}.results and Person.search{without(:middle_name, "* TO *")}.results, both of which return an empty set: [].
Anyone know a way that actually works and/or what the best way to do this is?

To make it work you have make monkey patch Sunspot::Query::Restriction::EqualTo method. Create a new file in config/initializers directory and add this code:
module Sunspot
module Query
module Restriction
class EqualTo < Base
def to_positive_boolean_phrase
case #value
when nil
"#{escape(#field.indexed_name)}:[* TO *]"
when ''
%Q(#{escape(#field.indexed_name)}:[* TO ""])
else
super
end
end
def negated?
if #value.nil?
!super
else
super
end
end
private
def to_solr_conditional
"#{solr_value}"
end
end
end
end
end
Remember to restart rails server before you try this.

Try this:
person = Person.solr_search do
keywords params[:middle_name]
end
person.results
If you want to try in console then, replace params[:middle_name]
with middle name of your choice. eg 'burger'

Related

Use Enumerators in Rails 7 with MongoDB

I'm building a new project with Rails 7 and MongoDB 8. And I wanted to use enumerators for multiple fields ( states etc .. )
I wanted to use the gem mongoid-enum but it doesn't work with Mongo 8.
Is switching to SQL database a solution ? Or is there any other way ?
I've checked on Mongo's doc and found a Phantom Custom Field Types but it looks like it's not saving in the db. In the rails console, I'll do the Model.status = "open" then saving it, it doesn't return any errors. So I close the console then open it again. Run the Model.status and it returns nil.
Thank you for reading and trying to help me !
First of all, there is good and bad in both MongoDB and PostgreSQL, it depends of the kind of features you need, see: https://www.geeksforgeeks.org/difference-between-postgresql-and-mongodb/
About the Phantom Custom Field Types, this is indeed doing the same stuff than ActiveRecord::Enum but asking more code to write. Could you share the not working code that you've run for your test please ?
Edit 07/02/22:
Here is an example of you could use enum in mongo without writing too much code:
module MongoEnum
# Takes application-scope value and converts it to how it would be
# stored in the database. Converts invalid values to nil.
def mongoize(object)
mapping[object]
end
# Get the value as it was stored in the database, and convert to
# application-scope value. Converts invalid values to nil.
def demongoize(object)
inverse_mapping[object]
end
# Converts the object that was supplied to a criteria and converts it
# into a query-friendly form. Returns invalid values as is.
def evolve(object)
mapping.fetch(object, object)
end
def mapping
#mapping ||= self.const_get(:MAPPING).freeze
end
def inverse_mapping
#inverse_mapping ||= mapping.invert.freeze
end
end
class RoleEnum
extend MongoEnum
MAPPING = {
'admin' => 0,
'user' => 1,
}.freeze
end
class ColorEnum
extend MongoEnum
MAPPING = {
'black' => 0,
'white' => 1,
}.freeze
end
class Profile
include Mongoid::Document
field :color, type: ColorEnum
end
class User
include Mongoid::Document
field :role, type: RoleEnum
end
Disclaimer: I didn't test it in a real app, let me know if it does not work.

Ruby on Rails: proper method of validating Model data comparing with parameter(s)

Using rails version 4.0.5 currently. I can't seem to find a canonical answer to whether this is the best (DRY, MVC, etc.) method to validate what seems to me to be a simple validation. In my model, I have a unique document_id. This document_id should appear in the filename (in this case, always xml files) e.g. blah/blah.document_id.xml and it should also appear as an attribute on the root element of the xml document e.g. id='document_id'. I'd like to write (assuming the document_id and filename don't match):
doc = Document.new
...
if !doc.valid?
puts doc.errors[:document_id] # <= 'document_id does not match filename'
end
Here's the closest I've gotten:
doc = Document.new
...
if !doc.valid?
... # no error caught here
end
if doc.document_id_matches_filename(xml_file)
puts 'document_id does not match filename'
end
Inside app/models/document.rb, I have this:
class Document < ActiveRecord::Base
...
def document_id_matches_filename(filename)
return self.document_id == File.basename(filename, '.xml')
end
end
I could add a filename column to the Document model and use doc.valid with a custom validator, but I don't want to effectively store the document_id twice. I can find information on sending parameters to custom validators or creating Validation classes, but those all seem to do things like use Time.now or things like that -- dynamic information but not specific information related to the document. I could try to find the xml file based on the doc id in a custom validator, but my rake task or Controller already has this information, it just needs to hand it off to the Model to validate against. Theoretically this would either be a rake task or on the Controller, I can't imagine that mattering.
Thanks in advance.
How about using a virtual attribute to store the filename so your model has access?
Something like...
class Document < ActiveRecord::Base
attr_accessor :filename
validate :document_id_matches_filename
private
def document_id_matches_filename
errors.add(:document_id, 'does not match filename') unless self.document_id == File.basename(self.filename, '.xml')
end
end
That allows you to use ActiveModel's validations and get the error message you're looking for and encapsulate it all within the model without having to add an extra DB column.

Store functions in mongodb using Mongoid 3

Just as the title suggests. I am not able to find anything related to Mongoid 3. The things I found only apply to old versions of mongoid that didn't use Moped.
I found this and it doesn't work:
def self.install_javascript
getWeekJs = Rails.root.join("lib/javascript/getWeek.js")
if collection.master['system.js'].find_one({'_id' => "getWeek"}).nil?
collection.master.db.add_stored_function("getWeek", File.new(getWeekJs).read)
end
end
This method would add a getWeek function to the system.js collection.
How can this be done in mongoid 3?
Nailed it!
Codes:
class StoredProcedure
include Mongoid::Document
store_in collection: "system.js"
field :_id, type: String, default: ""
def self.test
equalsJS = Rails.root.join("lib/assets/javascripts/equals.js")
code = Moped::BSON::Code.new(File.new(equalsJS).read)
unless where(id: "equals").first
proc = new(value: code)
proc._id = "equals"
proc.save
end
end
end
Explanation:
I'm using the system.js in mongoid as if it were a normal collection. I'm then simply adding new documents.
IMPORTANT:
The value needs to be a Moped::BSON::Code instance otherwise it will be saved as string, thus useless. The id needs to be the function's name. I wasn't able to specify the id in a create statement, therefore I added multiple steps.
Just add this to a rake task to make sure you add all required functions to mongo after deployment.

hash instead of id

I want to use auto-generated hash'es instead of auto-incremented integers in my activerecords as primary keys. This raises two questions:
how to perform this generation in
most efficient way?
how to handle possibility that
generated hash exists already in
table?
Regards,
Mateusz
If you want this because you don't want to show the id in the web url. You can use a gem like https://github.com/peterhellberg/hashids.rb
It creates a reversible hash from your database id so the hash does not need to be stored in the database.
Use it in your models to_param method.
class MyModel < ActiveRecord::Base
def to_param
Hashids.new("salt").encode(id)
end
end
And decode the hash before finding the record from the database.
def show
id = Hashids.new("salt").decode(params[:id]).try(:first)
record = MyModel.find(id)
end
It might not be exactly what you asked for. But I had a similar problem where I wanted to use a hash instead of the ID in the URL. My solution follows
I added a column in my table called privatelink
In my model i wrote:
#changes the url to use privatelink instead of the id
def to_param
privatelink
end
#calls the private method set_privatelink
before_create :set_privatelink
private
#generates a unique hash looking something like this: c24bea1693d9e56a1878cb83f252fba05532d9d0
def set_privatelink
self.privatelink = Digest::SHA1.hexdigest([Time.now, rand].join)
end
Source:
Railcast #63 Model Name in URL - shows how to use the to_param method
It's not a duplicate of your question, but i think you want to do the same thing :
Assigning Each User a Unique 100 character Hash in Ruby on Rails
When using Oracle i had the case where I wanted to create the ID ourselves (and not use a sequence), and in this post i provide the details how i did that. In short the code:
# a small patch as proposed by the author of OracleEnhancedAdapter: http://blog.rayapps.com/2008/05/13/activerecord-oracle-enhanced-adapter/#comment-240
# if a ActiveRecord model has a sequence with name "autogenerated", the id will not be filled in from any sequence
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
alias_method :orig_next_sequence_value, :next_sequence_value
def next_sequence_value(sequence_name)
if sequence_name == 'autogenerated'
# we assume id must have gotten a good value before insert!
id
else
orig_next_sequence_value(sequence_name)
end
end
end
while this solution is specific to Oracle-enhanced, i am assuming inside the other adapters you can overrule the same method (next_sequence_value).

Accessing model properties in Rails

So basically I have a controller. something like this
def show
#user = User.find[:params[id]]
#code to show in a view
end
User has properties such as name, address, gender etc. How can I access these properties in the model? Can I overload the model accesser for name for example and replace it with my own value or concatenate something to it. Like in the show.html.erb view for this method I might want to concatenate the user's name with 'Mr.' or 'Mrs.' depending upon the gender? How is it possible?
I would hesitate to override the attributes, and instead add to the model like this:
def titled_name
"#{title} #{name}"
end
However, you can access the fields directly like this:
def name
"#{title} #{self[:name]}"
end
You can create virtual attributes within your model to represent these structures.
There is a railscast on this very subject but in summary you can do something like this in your model
def full_name
[first_name, last_name].join(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
If you wish to explicitly change the value of an attribute when reading or writing then you can use the read_attribute or write_attribute methods. (Although I believe that these may be deprecated).
These work by replacing the accessor method of the attribute with your own. As an example, a branch identifier field can be entered as either xxxxxx or xx-xx-xx. So you can change your branch_identifier= method to remove the hyphens when the data is stored in the database. This can be achieved like so
def branch_identifier=(value)
write_attribute(:branch_identifier, value.gsub(/-/, '')) unless value.blank?
end
If you are accessing data stored directly in the database you can do this in you view:
<%= #user.firstname %>
<%= #user.gender %>
etc.
If you need to build custom representations of the data, then you will either need to create helpers, or extend the model (as above).
I tend to use helper methods added to the model for things like that:
def formatted_name
"#{title} #{first_name} #{last_name}"
end
(Edit previous post. Looked back at my code and realized helpers are supposed to be for presentation-related (mark-up) stuff only.)
(Edit again to remove left-over parameter... Geez, not enough coffee this morning.)
(Edit again to replace $ with #... Perhaps I should just remove this one huh?)
You can easily overload the attributes as you suggest.
i.e. if name is a field in the users database table, you can do:
def name
"#{title} #{read_attribute[:name]}"
end
The read_attribute function will return the database column value for the field.
Caveat: I am not sure this is a good idea. If you want a method that displays model data in a modified way, I would be tempted not to overload the default methods, and call them something different - this will avoid a certain level of obfuscation.
Documentation here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html (under 'Overwriting default accessors')
in http://api.rubyonrails.org/classes/ActionController/Base.html
search for
Overwriting default accessors

Resources