blacklight_iiif_search 0.0.1.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +46 -0
  3. data/app/controllers/concerns/blacklight_iiif_search/controller.rb +48 -0
  4. data/app/models/blacklight_iiif_search/iiif_search.rb +36 -0
  5. data/app/models/blacklight_iiif_search/iiif_search_annotation.rb +43 -0
  6. data/app/models/blacklight_iiif_search/iiif_search_response.rb +103 -0
  7. data/app/models/blacklight_iiif_search/iiif_suggest_response.rb +56 -0
  8. data/app/models/blacklight_iiif_search/iiif_suggest_search.rb +37 -0
  9. data/app/models/concerns/blacklight_iiif_search/annotation_behavior.rb +28 -0
  10. data/app/models/concerns/blacklight_iiif_search/ignored.rb +13 -0
  11. data/app/models/concerns/blacklight_iiif_search/search_behavior.rb +15 -0
  12. data/lib/blacklight_iiif_search.rb +14 -0
  13. data/lib/blacklight_iiif_search/engine.rb +14 -0
  14. data/lib/blacklight_iiif_search/routes.rb +12 -0
  15. data/lib/blacklight_iiif_search/version.rb +3 -0
  16. data/lib/generators/blacklight_iiif_search/controller_generator.rb +37 -0
  17. data/lib/generators/blacklight_iiif_search/install_generator.rb +52 -0
  18. data/lib/generators/blacklight_iiif_search/model_generator.rb +17 -0
  19. data/lib/generators/blacklight_iiif_search/routes_generator.rb +26 -0
  20. data/lib/generators/blacklight_iiif_search/solr_generator.rb +87 -0
  21. data/lib/generators/blacklight_iiif_search/templates/iiif_search_builder.rb +15 -0
  22. data/lib/generators/blacklight_iiif_search/templates/solr/lib/solr-tokenizing_suggester-7.x.jar +0 -0
  23. data/lib/railties/blacklight_iiif_search.rake +15 -0
  24. data/spec/controllers/catalog_controller_spec.rb +99 -0
  25. data/spec/fixtures/sample_solr_documents.yml +272 -0
  26. data/spec/iiif_search_shared.rb +49 -0
  27. data/spec/models/blacklight_iiif_search/iiif_search_annotation_spec.rb +36 -0
  28. data/spec/models/blacklight_iiif_search/iiif_search_response_spec.rb +73 -0
  29. data/spec/models/blacklight_iiif_search/iiif_search_spec.rb +23 -0
  30. data/spec/models/blacklight_iiif_search/iiif_suggest_response_spec.rb +43 -0
  31. data/spec/models/blacklight_iiif_search/iiif_suggest_search_spec.rb +36 -0
  32. data/spec/models/concerns/blacklight_iiif_search/annotation_behavior_spec.rb +31 -0
  33. data/spec/models/concerns/blacklight_iiif_search/ignored_spec.rb +10 -0
  34. data/spec/models/concerns/blacklight_iiif_search/search_behavior_spec.rb +12 -0
  35. data/spec/spec_helper.rb +26 -0
  36. data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -0
  37. metadata +238 -0
@@ -0,0 +1,14 @@
1
+ # BlacklightIiifSearch
2
+ module BlacklightIiifSearch
3
+ require 'blacklight_iiif_search/version'
4
+ require 'blacklight_iiif_search/engine'
5
+ require 'iiif/presentation'
6
+ require 'blacklight'
7
+
8
+ autoload :Routes, 'blacklight_iiif_search/routes'
9
+
10
+ # returns the full path the the blacklight plugin installation
11
+ def self.root
12
+ @root ||= File.expand_path(File.dirname(File.dirname(__FILE__)))
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module BlacklightIiifSearch
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace BlacklightIiifSearch
4
+
5
+ # Load rake tasks.
6
+ rake_tasks do
7
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..'))) do
8
+ Dir.glob(File.join('railties', '*.rake')).each do |railtie|
9
+ load railtie
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module BlacklightIiifSearch
2
+ class Routes
3
+ def initialize(defaults = {})
4
+ @defaults = defaults
5
+ end
6
+
7
+ def call(mapper, _options = {})
8
+ mapper.get 'iiif_search', action: 'iiif_search'
9
+ mapper.get 'iiif_suggest', action: 'iiif_suggest'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module BlacklightIiifSearch
2
+ VERSION = '0.0.1-alpha'.freeze
3
+ end
@@ -0,0 +1,37 @@
1
+ # adds controller-scope behavior to the implementing application
2
+ require 'rails/generators'
3
+
4
+ module BlacklightIiifSearch
5
+ class ControllerGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ argument :controller_name, type: :string, default: 'catalog'
9
+
10
+ desc "
11
+ This generator makes the following changes to your app's CatalogController:
12
+ 1. Includes BlacklightIiifSearch::Controller
13
+ 2. Adds some basic configuration settings in the configure_blacklight block
14
+ "
15
+
16
+ # Update the blacklight catalog controller
17
+ def inject_catalog_controller_behavior
18
+ return if IO.read("app/controllers/#{controller_name}_controller.rb").include?('BlacklightIiifSearch')
19
+ marker = 'include Blacklight::Catalog'
20
+ insert_into_file "app/controllers/#{controller_name}_controller.rb", after: marker do
21
+ "\n\n # CatalogController-scope behavior and configuration for BlacklightIiifSearch
22
+ include BlacklightIiifSearch::Controller"
23
+ end
24
+ marker = 'configure_blacklight do |config|'
25
+ insert_into_file "app/controllers/#{controller_name}_controller.rb", after: marker do
26
+ "\n\n # configuration for Blacklight IIIF Content Search
27
+ config.iiif_search = {
28
+ full_text_field: 'text',
29
+ object_relation_field: 'is_page_of_s',
30
+ supported_params: %w[q page],
31
+ autocomplete_handler: 'iiif_suggest',
32
+ suggester_name: 'iiifSuggester'
33
+ }\n"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ # install BlacklightIiifSearch behavior into implementing application
2
+ require 'rails/generators'
3
+
4
+ module BlacklightIiifSearch
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ argument :search_builder_name, type: :string, default: 'search_builder'
9
+ argument :controller_name, type: :string, default: 'catalog'
10
+
11
+ class_option :'skip-solr', type: :boolean,
12
+ default: false, desc: 'Skip generating Solr configurations.'
13
+
14
+ desc <<-EOS
15
+ Install generator for Blacklight IIIF Search
16
+ This generator makes the following changes to your application:
17
+ 1. Injects behavior into CatalogController
18
+ 2. Adds a SearchBuilder to ./app/models
19
+ 3. Adds BlacklightIiifSearch routes to ./config/routes.rb
20
+ 4. Modifies solrconfig.xml to support contextual autocomplete functionality
21
+ Thanks for installing Blacklight IIIF Search!
22
+ EOS
23
+
24
+ def verify_blacklight_installed
25
+ return if IO.read('app/controllers/application_controller.rb').include?('include Blacklight::Controller')
26
+ say_status('info', 'BLACKLIGHT NOT INSTALLED; GENERATING BLACKLIGHT', :blue)
27
+ generate 'blacklight:install'
28
+ end
29
+
30
+ def insert_to_controllers
31
+ generate 'blacklight_iiif_search:controller', controller_name
32
+ end
33
+
34
+ def insert_to_models
35
+ generate 'blacklight_iiif_search:model', search_builder_name
36
+ end
37
+
38
+ def insert_to_routes
39
+ generate 'blacklight_iiif_search:routes'
40
+ end
41
+
42
+ def add_solr_config
43
+ generate 'blacklight_iiif_search:solr' unless options[:'skip-solr']
44
+ end
45
+
46
+ def bundle_install
47
+ Bundler.with_clean_env do
48
+ run 'bundle install'
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ # add the SearchBuilder to the implementing application
2
+ require 'rails/generators'
3
+
4
+ module BlacklightIiifSearch
5
+ class ModelGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ argument :search_builder_model, type: :string, default: 'search_builder'
9
+
10
+ desc 'This generator makes the following changes to your app:
11
+ 1. Adds iiif_search_builder.rb to app/models'
12
+
13
+ def inject_search_builder_behavior
14
+ copy_file 'iiif_search_builder.rb', 'app/models/iiif_search_builder.rb'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # insert routing for IIIF Content Search
2
+ require 'rails/generators'
3
+
4
+ module BlacklightIiifSearch
5
+ class RoutesGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ desc 'This generator makes the following changes to your app:
9
+ 1. Injects route declarations into your routes.rb'
10
+
11
+ # Add CommonwealthVlrEngine to the routes
12
+ def inject_iiif_search_routes
13
+ unless IO.read('config/routes.rb').include?('BlacklightIiifSearch::Routes')
14
+ marker = 'Rails.application.routes.draw do'
15
+ insert_into_file 'config/routes.rb', after: marker do
16
+ "\n\n concern :iiif_search, BlacklightIiifSearch::Routes.new"
17
+ end
18
+ # for blacklight_range_limit
19
+ bl_routes_marker = /resources :solr_documents[\S\s]*controller: 'catalog' do[\s]*concerns :exportable.*$/
20
+ inject_into_file 'config/routes.rb', after: bl_routes_marker do
21
+ "\n concerns :iiif_search"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,87 @@
1
+ # adds iiif_suggest configuration to Solr solrconfig.xml and schema.xml
2
+ # to allow for suggestions limited by a contextField
3
+ require 'rails/generators'
4
+
5
+ module BlacklightIiifSearch
6
+ class SolrGenerator < Rails::Generators::Base
7
+ source_root ::File.expand_path('../templates', __FILE__)
8
+
9
+ desc 'Adds iiif_suggest configuration to Solr solrconfig.xml and schema.xml'
10
+
11
+ def inject_iiif_suggest_solrconfig
12
+ unless IO.read('solr/conf/solrconfig.xml').include?('iiif_suggest')
13
+ marker = '</config>'
14
+ insert_into_file 'solr/conf/solrconfig.xml', before: marker do
15
+ " <!-- BEGIN Blacklight IIIF Search autocomplete config -->
16
+ <!-- solr-tokenizing_suggester is necessary to return single terms from the suggester -->
17
+ <lib dir=\"${solr.install.dir:../../../..}/contrib\" regex=\"solr-tokenizing_suggester-7.x.jar\" />\n
18
+ <searchComponent name=\"iiif_suggest\" class=\"solr.SuggestComponent\">
19
+ <lst name=\"suggester\">
20
+ <str name=\"name\">iiifSuggester</str>
21
+ <str name=\"lookupImpl\">edu.stanford.dlss.search.suggest.analyzing.TokenizingLookupFactory</str>
22
+ <str name=\"dictionaryImpl\">DocumentDictionaryFactory</str>
23
+ <str name=\"suggestAnalyzerFieldType\">textSuggest</str>
24
+ <str name=\"suggestTokenizingAnalyzerFieldType\">textSuggestTokenizer</str>
25
+ <str name=\"contextField\">is_page_of_s</str>
26
+ <str name=\"buildOnCommit\">true</str>
27
+ <str name=\"field\">iiif_suggest</str>
28
+ </lst>
29
+ </searchComponent>\n
30
+ <requestHandler name=\"/iiif_suggest\" class=\"solr.SearchHandler\" startup=\"lazy\">
31
+ <lst name=\"defaults\">
32
+ <str name=\"suggest\">true</str>
33
+ <str name=\"suggest.count\">5</str>
34
+ <str name=\"suggest.dictionary\">iiifSuggester</str>
35
+ </lst>
36
+ <arr name=\"components\">
37
+ <str>iiif_suggest</str>
38
+ </arr>
39
+ </requestHandler>
40
+ <!-- END Blacklight IIIF Search autocomplete config -->\n\n"
41
+ end
42
+ end
43
+ end
44
+
45
+ def inject_iiif_suggest_schema
46
+ filepath = 'solr/conf/schema.xml'
47
+ unless IO.read(filepath).include?('iiif_suggest')
48
+ field_type_marker = '</types>'
49
+ insert_into_file filepath, before: field_type_marker do
50
+ "\n <!-- BEGIN Blacklight IIIF Search autocomplete config -->
51
+ <fieldType name=\"textSuggestTokenizer\" class=\"solr.TextField\" positionIncrementGap=\"100\">
52
+ <analyzer>
53
+ <tokenizer class=\"solr.WhitespaceTokenizerFactory\"/>
54
+ <filter class=\"solr.StopFilterFactory\" ignoreCase=\"true\" words=\"stopwords_en.txt\"/>
55
+ <filter class=\"solr.WordDelimiterGraphFilterFactory\"/>
56
+ <filter class=\"solr.LowerCaseFilterFactory\"/>
57
+ <filter class=\"solr.HyphenatedWordsFilterFactory\"/>
58
+ <filter class=\"solr.RemoveDuplicatesTokenFilterFactory\"/>
59
+ <!-- uncomment below to enable multi-word matches -->
60
+ <!-- <filter class=\"solr.ShingleFilterFactory\" outputUnigrams=\"true\" outputUnigramsIfNoShingles=\"true\" maxShingleSize=\"3\" /> -->
61
+ </analyzer>
62
+ </fieldType>
63
+ <!-- END Blacklight IIIF Search autocomplete config -->\n\n"
64
+ end
65
+
66
+ fields_marker = '</fields>'
67
+ insert_into_file filepath, before: fields_marker do
68
+ " <!-- BEGIN Blacklight IIIF Search autocomplete config -->
69
+ <field name=\"iiif_suggest\" type=\"textSuggest\" indexed=\"true\" stored=\"true\" multiValued=\"true\" />
70
+ <!-- END Blacklight IIIF Search autocomplete config -->\n\n"
71
+ end
72
+
73
+ copy_marker = '</schema>'
74
+ insert_into_file filepath, before: copy_marker do
75
+ " <!-- BEGIN Blacklight IIIF Search autocomplete config -->
76
+ <copyField source=\"text\" dest=\"iiif_suggest\"/>
77
+ <!-- END Blacklight IIIF Search autocomplete config -->\n\n"
78
+ end
79
+ end
80
+ end
81
+
82
+ def copy_suggester_library
83
+ copy_file 'solr/lib/solr-tokenizing_suggester-7.x.jar',
84
+ 'solr/lib/solr-tokenizing_suggester-7.x.jar'
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,15 @@
1
+ # SearchBuilder for full-text searches with highlighting and snippets
2
+ class IiifSearchBuilder < Blacklight::SearchBuilder
3
+ include Blacklight::Solr::SearchBuilderBehavior
4
+
5
+ self.default_processor_chain += [:ocr_search_params]
6
+
7
+ # set params for ocr field searching
8
+ def ocr_search_params(solr_parameters = {})
9
+ solr_parameters[:facet] = false
10
+ solr_parameters[:hl] = true
11
+ solr_parameters[:'hl.fl'] = blacklight_config.iiif_search[:full_text_field]
12
+ solr_parameters[:'hl.fragsize'] = 100
13
+ solr_parameters[:'hl.snippets'] = 10
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ namespace :blacklight_iiif_search do
2
+ namespace :index do
3
+ desc 'Put sample data into solr'
4
+ task seed: [:environment] do
5
+ require 'yaml'
6
+ docs = YAML.safe_load(File.open(File.join(BlacklightIiifSearch.root,
7
+ 'spec',
8
+ 'fixtures',
9
+ 'sample_solr_documents.yml')))
10
+ conn = Blacklight.default_index.connection
11
+ conn.add docs
12
+ conn.commit
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,99 @@
1
+ require 'iiif_search_shared'
2
+ RSpec.describe CatalogController do
3
+ include_context 'iiif_search_shared'
4
+
5
+ render_views
6
+
7
+ describe 'controller methods' do
8
+ describe '#iiif_search' do
9
+ it 'responds to iiif_search' do
10
+ expect(controller).to respond_to :iiif_search
11
+ end
12
+ end
13
+
14
+ describe '#iiif_suggest' do
15
+ it 'responds to iiif_suggest' do
16
+ expect(controller).to respond_to :iiif_suggest
17
+ end
18
+ end
19
+
20
+ describe '#iiif_search_config' do
21
+ subject { controller.iiif_search_config }
22
+ it 'returns the iiif_search config' do
23
+ expect(subject[:full_text_field]).to_not be_falsey
24
+ expect(subject[:object_relation_field]).to_not be_falsey
25
+ expect(subject[:supported_params]).to_not be_empty
26
+ end
27
+ end
28
+
29
+ describe '#iiif_search_params' do
30
+ before do
31
+ controller.params = { q: query_term, foo: 'bar' }
32
+ end
33
+
34
+ subject { controller.iiif_search_params }
35
+ it 'only returns the permitted params' do
36
+ expect(subject.include?(:q)).to be_truthy
37
+ expect(subject.include?(:foo)).to be_falsey
38
+ end
39
+ end
40
+
41
+ describe 'before and after actions' do
42
+ before do
43
+ get :iiif_search,
44
+ params: { q: query_term, solr_document_id: parent_id }
45
+ end
46
+
47
+ describe '#set_search_builder' do
48
+ it 'should use IiifSearchBuilder' do
49
+ expect(controller.blacklight_config.search_builder_class).to eq(IiifSearchBuilder)
50
+ end
51
+ end
52
+
53
+ describe '#set_access_headers' do
54
+ it 'should set the access control header' do
55
+ expect(response.headers['Access-Control-Allow-Origin']).to eq('*')
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ describe 'render response' do
62
+ describe 'GET :iiif_search' do
63
+ before do
64
+ get :iiif_search,
65
+ params: { q: query_term, solr_document_id: parent_id }
66
+ end
67
+ let(:json) { JSON.parse(response.body) }
68
+
69
+ it 'returns a response' do
70
+ expect(response).to be_success
71
+ end
72
+
73
+ it 'returns a IIIF AnnotationList' do
74
+ expect(json['@type']).to eq('sc:AnnotationList')
75
+ expect(json['resources']).not_to be_blank
76
+ expect(json['within']).not_to be_blank
77
+ expect(json['hits']).not_to be_blank
78
+ end
79
+ end
80
+
81
+ describe 'GET :iiif_search' do
82
+ before do
83
+ get :iiif_suggest,
84
+ params: { q: suggest_query_term, solr_document_id: parent_id }
85
+ end
86
+ let(:json) { JSON.parse(response.body) }
87
+
88
+ it 'returns a response' do
89
+ expect(response).to be_success
90
+ end
91
+
92
+ it 'returns a IIIF TermList' do
93
+ expect(json['@type']).to eq('search:TermList')
94
+ expect(json['terms']).not_to be_blank
95
+ expect(json['terms'].first['match']).not_to be_nil
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,272 @@
1
+ # Sample Solr docs for full-text search, representing book and pages.
2
+ # We duplicate full text in isbn_t field since this is the only text field
3
+ # that is indexed and stored in schema.xml AND searched by default
4
+ # requestHandler in solrconfig.xml in out-of-box Blacklight 6.* Solr config,
5
+ # which allows us to test highlighting and snippets.
6
+ ---
7
+ - object_model_s:
8
+ - Book
9
+ id:
10
+ - 7s75dn48d
11
+ label_s:
12
+ - Raw denim raclette hexagon
13
+ title_t:
14
+ - Raw denim raclette hexagon
15
+ title_display: Raw denim raclette hexagon
16
+ author_t:
17
+ - Doe, Jane
18
+ author_display: Jane Doe
19
+ has_searchable_text_b:
20
+ - true
21
+ text_direction_s:
22
+ - ltr
23
+ timestamp:
24
+ - '2015-11-18T21:22:14.23Z'
25
+ - object_model_s:
26
+ - ImageFile
27
+ id:
28
+ - 7s75dn59x
29
+ is_file_of_s:
30
+ - 7s75dn48d
31
+ is_following_page_of_s:
32
+ - 7s75dn58n
33
+ is_page_of_s:
34
+ - 7s75dn48d
35
+ is_preceding_page_of_s:
36
+ - 7s75dn60x
37
+ page_type_s:
38
+ - Normal
39
+ hand_side_s:
40
+ - RIGHT
41
+ page_num_label_s:
42
+ - 5
43
+ page_num_label_type_s:
44
+ - original
45
+ text:
46
+ - In selecting sugar the confectioner must bear in mind that it is the foundation of all confectionery, and can not expect to manufacture iirst-class goods except from first-class material; hence, select only sugar that is perfectly dry and of uniform quality, and in white, hard, sparkling crystals; only such sugar will give satisfaction.
47
+ isbn_t:
48
+ - In selecting sugar the confectioner must bear in mind that it is the foundation of all confectionery, and can not expect to manufacture iirst-class goods except from first-class material; hence, select only sugar that is perfectly dry and of uniform quality, and in white, hard, sparkling crystals; only such sugar will give satisfaction.
49
+ timestamp:
50
+ - '2015-11-18T21:09:44.574Z'
51
+ - object_model_s:
52
+ - Book
53
+ id:
54
+ - 3j334603p
55
+ label_s:
56
+ - Quinoa gastropub snackwave
57
+ title_t:
58
+ - Quinoa gastropub snackwave
59
+ title_display: Quinoa gastropub snackwave
60
+ author_t:
61
+ - Doe, John
62
+ author_display: John Doe
63
+ has_searchable_text_b:
64
+ - true
65
+ text_direction_s:
66
+ - ltr
67
+ timestamp:
68
+ - '2015-11-18T19:32:49.014Z'
69
+ - object_model_s:
70
+ - ImageFile
71
+ id:
72
+ - 3j334628j
73
+ is_file_of_s:
74
+ - 3j334603p
75
+ is_page_of_s:
76
+ - 3j334603p
77
+ page_type_s:
78
+ - Normal
79
+ hand_side_s:
80
+ - RIGHT
81
+ page_num_label_s:
82
+ - 1
83
+ page_num_label_type_s:
84
+ - original
85
+ text:
86
+ - In choosing Colors for candy, certain qualifications are necessary. First, they must not fade or change when exposed to the light. Second, they must not be easily affected by acids or alkalies; hence, it has been difficult to produce colors that are reasonably permanent and at the same time harmless. Yet the following will be found as near perfect as any yet produced.
87
+ isbn_t:
88
+ - In choosing Colors for candy, certain qualifications are necessary. First, they must not fade or change when exposed to the light. Second, they must not be easily affected by acids or alkalies; hence, it has been difficult to produce colors that are reasonably permanent and at the same time harmless. Yet the following will be found as near perfect as any yet produced.
89
+ timestamp:
90
+ - '2015-11-18T17:27:35.361Z'
91
+ - object_model_s:
92
+ - ImageFile
93
+ id:
94
+ - 7s75dn58n
95
+ is_file_of_s:
96
+ - 7s75dn48d
97
+ # is_following_page_of_s:
98
+ # - 7s75dn57c
99
+ is_page_of_s:
100
+ - 7s75dn48d
101
+ is_preceding_page_of_s:
102
+ - 7s75dn59x
103
+ page_type_s:
104
+ - Normal
105
+ hand_side_s:
106
+ - LEFT
107
+ page_num_label_s:
108
+ - 4
109
+ page_num_label_type_s:
110
+ - original
111
+ text:
112
+ - Twelve pounds of sugar, two quarts of water, stir until dissolved, then add one full teaspoon of cream of tartar, (or if you wish to use glucose, add three pounds of same, do this when you are weighing your sugar, by hollowing out a place in the center of the sugar and pouring in the glucose). Now put on the steamer and let it remain until the batch begins to cook, then remove; in this way all the grains of sugar adhereing to the sides of the basin are washed down.
113
+ isbn_t:
114
+ - Twelve pounds of sugar, two quarts of water, stir until dissolved, then add one full teaspoon of cream of tartar, (or if you wish to use glucose, add three pounds of same, do this when you are weighing your sugar, by hollowing out a place in the center of the sugar and pouring in the glucose). Now put on the steamer and let it remain until the batch begins to cook, then remove; in this way all the grains of sugar adhereing to the sides of the basin are washed down.
115
+ timestamp:
116
+ - '2015-11-18T21:09:30.2Z'
117
+ - object_model_s:
118
+ - ImageFile
119
+ id:
120
+ - 7s75dn60x
121
+ is_file_of_s:
122
+ - 7s75dn48d
123
+ is_following_page_of_s:
124
+ - 7s75dn59x
125
+ is_page_of_s:
126
+ - 7s75dn48d
127
+ is_preceding_page_of_s:
128
+ - 7s75dn61x
129
+ page_type_s:
130
+ - Normal
131
+ hand_side_s:
132
+ - LEFT
133
+ page_num_label_s:
134
+ - 6
135
+ page_num_label_type_s:
136
+ - original
137
+ text:
138
+ - when it begins to boil, add six pounds of Almond Nuts, after haying picked out all shells and dusted the nuts thoroughly; stir slowly, keeping the sides of the basin well washed, until nuts are as brown as wished, and slide off the spatula easily when raised up
139
+ isbn_t:
140
+ - when it begins to boil, add six pounds of Almond Nuts, after haying picked out all shells and dusted the nuts thoroughly; stir slowly, keeping the sides of the basin well washed, until nuts are as brown as wished, and slide off the spatula easily when raised up
141
+ timestamp:
142
+ - '2015-11-19T21:09:44.574Z'
143
+ - object_model_s:
144
+ - ImageFile
145
+ id:
146
+ - 7s75dn61x
147
+ is_file_of_s:
148
+ - 7s75dn48d
149
+ is_following_page_of_s:
150
+ - 7s75dn60x
151
+ is_page_of_s:
152
+ - 7s75dn48d
153
+ is_preceding_page_of_s:
154
+ - 7s75dn62x
155
+ page_type_s:
156
+ - Normal
157
+ hand_side_s:
158
+ - RIGHT
159
+ page_num_label_s:
160
+ - 7
161
+ page_num_label_type_s:
162
+ - original
163
+ text:
164
+ - These Caramel receipts are the best in the worlds and the author questions the ability of any one to produce a Caramel that will excell them in any particular. After once making by either one of the following formulas, no other will be used, as they are worth a dozen times the price of this book, to any one wishing to give his trade a fine Caramel.
165
+ isbn_t:
166
+ - These Caramel receipts are the best in the worlds and the author questions the ability of any one to produce a Caramel that will excell them in any particular. After once making by either one of the following formulas, no other will be used, as they are worth a dozen times the price of this book, to any one wishing to give his trade a fine Caramel.
167
+ timestamp:
168
+ - '2015-11-20T21:09:44.574Z'
169
+ - object_model_s:
170
+ - ImageFile
171
+ id:
172
+ - 7s75dn62x
173
+ is_file_of_s:
174
+ - 7s75dn48d
175
+ is_following_page_of_s:
176
+ - 7s75dn61x
177
+ is_page_of_s:
178
+ - 7s75dn48d
179
+ is_preceding_page_of_s:
180
+ - 7s75dn63x
181
+ page_type_s:
182
+ - Normal
183
+ hand_side_s:
184
+ - LEFT
185
+ page_num_label_s:
186
+ - 8
187
+ page_num_label_type_s:
188
+ - original
189
+ text:
190
+ - Same as Hickory-nut Cups, except make a center of almond paste, colored light green; when on the slab, add to the clear batch a few drops of red color, some Nectar flavor, and fold all together, pull on the hook to a bright pink, and finish as before.
191
+ isbn_t:
192
+ - Same as Hickory-nut Cups, except make a center of almond paste, colored light green; when on the slab, add to the clear batch a few drops of red color, some Nectar flavor, and fold all together, pull on the hook to a bright pink, and finish as before.
193
+ timestamp:
194
+ - '2015-11-21T21:09:44.574Z'
195
+ - object_model_s:
196
+ - ImageFile
197
+ id:
198
+ - 7s75dn63x
199
+ is_file_of_s:
200
+ - 7s75dn48d
201
+ is_following_page_of_s:
202
+ - 7s75dn62x
203
+ is_page_of_s:
204
+ - 7s75dn48d
205
+ is_preceding_page_of_s:
206
+ - 7s75dn64x
207
+ page_type_s:
208
+ - Normal
209
+ hand_side_s:
210
+ - RIGHT
211
+ page_num_label_s:
212
+ - 9
213
+ page_num_label_type_s:
214
+ - original
215
+ text:
216
+ - as it looks cloudy, pour it out at once in a deep pan; this must be done quickly or it will become hard in the basin; when cold, remove it it from the pan in the same manner as the pan creams; mark it into bars or large squares for the counter.
217
+ isbn_t:
218
+ - as it looks cloudy, pour it out at once in a deep pan; this must be done quickly or it will become hard in the basin; when cold, remove it it from the pan in the same manner as the pan creams; mark it into bars or large squares for the counter.
219
+ timestamp:
220
+ - '2015-11-22T21:09:44.574Z'
221
+ - object_model_s:
222
+ - ImageFile
223
+ id:
224
+ - 7s75dn64x
225
+ is_file_of_s:
226
+ - 7s75dn48d
227
+ is_following_page_of_s:
228
+ - 7s75dn63x
229
+ is_page_of_s:
230
+ - 7s75dn48d
231
+ is_preceding_page_of_s:
232
+ - 7s75dn65x
233
+ page_type_s:
234
+ - Normal
235
+ hand_side_s:
236
+ - LEFT
237
+ page_num_label_s:
238
+ - 10
239
+ page_num_label_type_s:
240
+ - original
241
+ text:
242
+ - cut into small squares; cook just to a thread; remove and, with the spatula, work the syrup on the sides of the basin till it becomes cloudy; pour all on a wire sieve, having a pan under it to catch the syrup that drains from the Cocoanuts
243
+ isbn_t:
244
+ - cut into small squares; cook just to a thread; remove and, with the spatula, work the syrup on the sides of the basin till it becomes cloudy; pour all on a wire sieve, having a pan under it to catch the syrup that drains from the Cocoanuts
245
+ timestamp:
246
+ - '2015-11-23T21:09:44.574Z'
247
+ - object_model_s:
248
+ - ImageFile
249
+ id:
250
+ - 7s75dn65x
251
+ is_file_of_s:
252
+ - 7s75dn48d
253
+ is_following_page_of_s:
254
+ - 7s75dn64x
255
+ is_page_of_s:
256
+ - 7s75dn48d
257
+ # is_preceding_page_of_s:
258
+ # - 7s75dn65x
259
+ page_type_s:
260
+ - Normal
261
+ hand_side_s:
262
+ - RIGHT
263
+ page_num_label_s:
264
+ - 11
265
+ page_num_label_type_s:
266
+ - original
267
+ text:
268
+ - Take of dipping Cream any number of pounds desired, say five; put it into a basin inside of another one containing water; place on the fire; stir the Cream until dissolved, but not thin; add a few drops of red color, and flavor Nectar or Strawberry
269
+ isbn_t:
270
+ - Take of dipping Cream any number of pounds desired, say five; put it into a basin inside of another one containing water; place on the fire; stir the Cream until dissolved, but not thin; add a few drops of red color, and flavor Nectar or Strawberry
271
+ timestamp:
272
+ - '2015-11-24T21:09:44.574Z'