neuralworm commited on
Commit
d07d4c9
1 Parent(s): 21790a2

better populate speed, refactor, fix

Browse files
Files changed (3) hide show
  1. app.py +237 -237
  2. gematria.db +0 -3
  3. util.py +18 -6
app.py CHANGED
@@ -10,263 +10,263 @@ from deep_translator import GoogleTranslator, exceptions
10
  from urllib.parse import quote_plus
11
 
12
  # Set up logging
13
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
 
15
- # Global variables for database connection and translator
16
  conn = None
17
  translator = None
18
- book_names = {} # Dictionary to store book names
19
-
20
- def flatten_text(text):
21
- """Helper function to flatten nested lists into a single list."""
22
- if isinstance(text, list):
23
- return " ".join(flatten_text(item) if isinstance(item, list) else item for item in text)
24
- return text
25
 
26
  def initialize_database():
27
- """Initializes the SQLite database."""
28
- global conn
29
- conn = sqlite3.connect('gematria.db')
30
- c = conn.cursor()
31
- c.execute('''
32
- CREATE TABLE IF NOT EXISTS results (
33
- gematria_sum INTEGER,
34
- words TEXT,
35
- translation TEXT,
36
- book INTEGER,
37
- chapter INTEGER,
38
- verse INTEGER,
39
- PRIMARY KEY (gematria_sum, book, chapter, verse)
40
- )
41
- ''')
42
- c.execute('''
43
- CREATE TABLE IF NOT EXISTS processed_books (
44
- book INTEGER PRIMARY KEY,
45
- max_phrase_length INTEGER
46
- )
47
- ''')
48
- conn.commit()
49
- logging.info("Database initialized.")
 
 
50
 
51
  def initialize_translator():
52
- """Initializes the Google Translator."""
53
- global translator
54
- translator = GoogleTranslator(source='iw', target='en')
55
- logging.info("Translator initialized.")
56
-
57
- def insert_phrase_to_db(gematria_sum, phrase_candidate, book, chapter, verse):
58
- """Inserts a phrase and its Gematria value into the database."""
59
- global conn
60
- c = conn.cursor()
61
- try:
62
- c.execute('''
63
- INSERT INTO results (gematria_sum, words, book, chapter, verse)
64
- VALUES (?, ?, ?, ?, ?)
65
- ''', (gematria_sum, phrase_candidate, book, chapter, verse))
66
- conn.commit()
67
- logging.debug(f"Inserted phrase: {phrase_candidate} (Gematria: {gematria_sum}) at {book}:{chapter}:{verse}")
68
- except sqlite3.IntegrityError:
69
- logging.debug(f"Phrase already exists: {phrase_candidate} (Gematria: {gematria_sum}) at {book}:{chapter}:{verse}")
70
 
71
  def populate_database(tanach_texts, max_phrase_length=1):
72
- """Populates the database with phrases from the Tanach and their Gematria values."""
73
- global conn, book_names
74
- logging.info("Populating database...")
75
- c = conn.cursor()
76
-
77
- for book_id, text in tanach_texts: # Unpack the tuple (book_id, text)
78
- c.execute('''SELECT max_phrase_length FROM processed_books WHERE book = ?''', (book_id,))
79
- result = c.fetchone()
80
- if result and result[0] >= max_phrase_length:
81
- logging.info(f"Skipping book {book_id}: Already processed with max_phrase_length {result[0]}")
82
- continue
83
-
84
- logging.info(f"Processing book {book_id} with max_phrase_length {max_phrase_length}")
85
- if 'text' not in text or not isinstance(text['text'], list):
86
- logging.warning(f"Skipping book {book_id} due to missing or invalid 'text' field.")
87
- continue
88
-
89
- title = text.get('title', 'Unknown')
90
- book_names[book_id] = title # Store book name
91
-
92
- chapters = text['text']
93
- for chapter_id, chapter in enumerate(chapters):
94
- if not isinstance(chapter, list):
95
- logging.warning(f"Skipping chapter {chapter_id} in book {title} due to invalid format.")
96
- continue
97
- for verse_id, verse in enumerate(chapter):
98
- verse_text = flatten_text(verse)
99
- verse_text = re.sub(r"[^\u05D0-\u05EA ]+", "", verse_text)
100
- verse_text = re.sub(r" +", " ", verse_text)
101
- words = verse_text.split()
102
- for length in range(1, max_phrase_length + 1):
103
- for start in range(len(words) - length + 1):
104
- phrase_candidate = " ".join(words[start:start + length])
105
- gematria_sum = calculate_gematria(phrase_candidate.replace(" ", ""))
106
- insert_phrase_to_db(gematria_sum, phrase_candidate, book_id, chapter_id + 1, verse_id + 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  try:
108
- c.execute('''INSERT INTO processed_books (book, max_phrase_length) VALUES (?, ?)''', (book_id, max_phrase_length))
 
 
 
 
 
109
  except sqlite3.IntegrityError:
110
- c.execute('''UPDATE processed_books SET max_phrase_length = ? WHERE book = ?''', (max_phrase_length, book_id))
111
- conn.commit()
112
- logging.info("Database population complete.")
113
 
114
  def get_translation(phrase):
115
- """Retrieves or generates the English translation of a Hebrew phrase."""
116
- global translator, conn
117
- c = conn.cursor()
118
- c.execute('''
119
- SELECT translation FROM results
120
- WHERE words = ?
121
- ''', (phrase,))
122
- result = c.fetchone()
123
- if result and result[0]:
124
- return result[0]
125
- else:
126
- translation = translate_and_store(phrase)
127
- c.execute('''
128
- UPDATE results
129
- SET translation = ?
130
- WHERE words = ?
131
- ''', (translation, phrase))
132
- conn.commit()
133
- return translation
134
-
135
 
136
  def translate_and_store(phrase):
137
- global translator
138
- max_retries = 3 # You can adjust the number of retries
139
- retries = 0
140
-
141
- while retries < max_retries:
142
- try:
143
- translation = translator.translate(phrase)
144
- logging.debug(f"Translated phrase: {translation}")
145
- return translation
146
- except (exceptions.TranslationNotFound, exceptions.NotValidPayload,
147
- exceptions.ServerException, exceptions.RequestError, requests.exceptions.ConnectionError) as e: # Add ConnectionError
148
- retries += 1
149
- logging.warning(f"Error translating phrase '{phrase}': {e}. Retrying... ({retries}/{max_retries})")
150
-
151
- logging.error(f"Failed to translate phrase '{phrase}' after {max_retries} retries.")
152
- return "[Translation Error]"
 
153
 
154
  def search_gematria_in_db(gematria_sum):
155
- """Searches the database for phrases with a given Gematria value."""
156
- global conn
157
- c = conn.cursor()
158
- c.execute('''
159
- SELECT words, book, chapter, verse FROM results WHERE gematria_sum = ?
160
- ''', (gematria_sum,))
161
- results = c.fetchall()
162
- logging.debug(f"Found {len(results)} matching phrases for Gematria: {gematria_sum}")
163
- return results
164
 
165
  def gematria_search_interface(phrase):
166
- """The main function for the Gradio interface."""
167
- if not phrase.strip():
168
- return "Please enter a phrase."
169
-
170
- # Create database connection inside the function
171
- global conn, book_names
172
- conn = sqlite3.connect('gematria.db')
173
- c = conn.cursor()
174
-
175
- phrase_gematria = calculate_gematria(phrase.replace(" ", ""))
176
- logging.info(f"Searching for phrases with Gematria: {phrase_gematria}")
177
-
178
- matching_phrases = search_gematria_in_db(phrase_gematria)
179
- if not matching_phrases:
180
- return "No matching phrases found."
181
-
182
- # Sort results by book, chapter, and verse
183
- sorted_phrases = sorted(matching_phrases, key=lambda x: (x[1], x[2], x[3]))
184
-
185
- # Group results by book
186
- results_by_book = defaultdict(list)
187
- for words, book, chapter, verse in sorted_phrases:
188
- results_by_book[book].append((words, chapter, verse))
189
-
190
- # Format results for display with enhanced structure
191
- results = []
192
- results.append("<div class='results-container'>")
193
- for book, phrases in results_by_book.items():
194
- results.append(f"<h4>Book: {book_names.get(book, 'Unknown')}</h4>")
195
- for words, chapter, verse in phrases:
196
- translation = get_translation(words)
197
- book_name_english = book_names.get(book, 'Unknown')
198
- link = f"https://www.biblegateway.com/passage/?search={quote_plus(book_name_english)}+{chapter}%3A{verse}"
199
-
200
- results.append(f"""
201
- <div class='result-item'>
202
- <p>Chapter: {chapter}, Verse: {verse}</p>
203
- <p class='hebrew-phrase'>Hebrew Phrase: {words}</p>
204
- <p>Translation: {translation}</p>
205
- <a href='{link}' target='_blank' class='bible-link'>[See on Bible Gateway]</a>
206
- </div>
207
- """)
208
- results.append("</div>") # Close results-container div
209
-
210
- conn.close()
211
-
212
- # Add CSS styling
213
- style = """
214
- <style>
215
- .results-container {
216
- display: grid;
217
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
218
- gap: 20px;
219
- }
220
-
221
- .result-item {
222
- border: 1px solid #ccc;
223
- padding: 15px;
224
- border-radius: 5px;
225
- box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
226
- }
227
-
228
- .hebrew-phrase {
229
- font-family: 'SBL Hebrew', 'Ezra SIL', serif;
230
- direction: rtl;
231
- }
232
-
233
- .bible-link {
234
- display: block;
235
- margin-top: 10px;
236
- color: #007bff;
237
- text-decoration: none;
238
- }
239
- </style>
240
- """
241
-
242
- return style + "\n".join(results) # Concatenate style and results
 
 
 
 
243
 
244
  def run_app():
245
- """Initializes and launches the Gradio app."""
246
- initialize_database()
247
- initialize_translator()
248
-
249
- # Pre-populate the database
250
-
251
- tanach_texts = process_json_files(27, 27) # Process all books
252
- populate_database(tanach_texts, max_phrase_length=12)
253
- tanach_texts = process_json_files(1, 39) # Process all books
254
- populate_database(tanach_texts, max_phrase_length=1)
255
- #tanach_texts = process_json_files(1, 1) # Process all books
256
- #populate_database(tanach_texts, max_phrase_length=4)
257
- #tanach_texts = process_json_files(27, 27) # Process all books
258
- #populate_database(tanach_texts, max_phrase_length=4)
259
-
260
- iface = gr.Interface(
261
- fn=gematria_search_interface,
262
- inputs=gr.Textbox(label="Enter phrase"),
263
- outputs=gr.HTML(label="Results"),
264
- title="Gematria Search in Tanach",
265
- description="Search for phrases in the Tanach that have the same Gematria value.",
266
- live=False,
267
- allow_flagging="never"
268
- )
269
- iface.launch()
270
 
271
  if __name__ == "__main__":
272
- run_app()
 
10
  from urllib.parse import quote_plus
11
 
12
  # Set up logging
13
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
14
 
15
+ # Global variables for database connection, translator and book names
16
  conn = None
17
  translator = None
18
+ book_names = {}
 
 
 
 
 
 
19
 
20
  def initialize_database():
21
+ """Initializes the SQLite database."""
22
+ global conn
23
+ conn = sqlite3.connect('gematria.db')
24
+ cursor = conn.cursor()
25
+
26
+ # Create tables if they don't exist
27
+ cursor.execute('''
28
+ CREATE TABLE IF NOT EXISTS results (
29
+ gematria_sum INTEGER,
30
+ words TEXT,
31
+ translation TEXT,
32
+ book INTEGER,
33
+ chapter INTEGER,
34
+ verse INTEGER,
35
+ PRIMARY KEY (gematria_sum, book, chapter, verse)
36
+ )
37
+ ''')
38
+ cursor.execute('''
39
+ CREATE TABLE IF NOT EXISTS processed_books (
40
+ book INTEGER PRIMARY KEY,
41
+ max_phrase_length INTEGER
42
+ )
43
+ ''')
44
+ conn.commit()
45
+ logging.info("Database initialized.")
46
 
47
  def initialize_translator():
48
+ """Initializes the Google Translator."""
49
+ global translator
50
+ translator = GoogleTranslator(source='iw', target='en')
51
+ logging.info("Translator initialized.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  def populate_database(tanach_texts, max_phrase_length=1):
54
+ """Populates the database with phrases from the Tanach and their Gematria values."""
55
+ global conn, book_names
56
+ logging.info("Populating database...")
57
+ cursor = conn.cursor()
58
+
59
+ for book_id, book_data in tanach_texts.items():
60
+ # Check if the book is already processed for this max_phrase_length
61
+ cursor.execute('''SELECT max_phrase_length FROM processed_books WHERE book = ?''', (book_id,))
62
+ result = cursor.fetchone()
63
+ if result and result[0] >= max_phrase_length:
64
+ logging.info(f"Skipping book {book_id}: Already processed with max_phrase_length {result[0]}")
65
+ continue
66
+
67
+ logging.info(f"Processing book {book_id} with max_phrase_length {max_phrase_length}")
68
+
69
+ if 'text' not in book_data or not isinstance(book_data['text'], list):
70
+ logging.warning(f"Skipping book {book_id} due to missing or invalid 'text' field.")
71
+ continue
72
+
73
+ title = book_data.get('title', 'Unknown')
74
+ book_names[book_id] = title
75
+
76
+ chapters = book_data['text']
77
+ for chapter_id, chapter in enumerate(chapters):
78
+ if not isinstance(chapter, list):
79
+ logging.warning(f"Skipping chapter {chapter_id} in book {title} due to invalid format.")
80
+ continue
81
+ for verse_id, verse in enumerate(chapter):
82
+ verse_text = flatten_text(verse)
83
+ # Remove text in square brackets
84
+ verse_text = re.sub(r'\[.*?\]', '', verse_text)
85
+ verse_text = re.sub(r"[^\u05D0-\u05EA ]+", "", verse_text)
86
+ verse_text = re.sub(r" +", " ", verse_text)
87
+ words = verse_text.split()
88
+
89
+ # Iterate through phrases of different lengths
90
+ for length in range(1, max_phrase_length + 1):
91
+ for start in range(len(words) - length + 1):
92
+ phrase_candidate = " ".join(words[start:start + length])
93
+ gematria_sum = calculate_gematria(phrase_candidate.replace(" ", ""))
94
+ insert_phrase_to_db(gematria_sum, phrase_candidate, book_id, chapter_id + 1, verse_id + 1)
95
+
96
+ # Mark the book as processed for this max_phrase_length
97
+ cursor.execute('''INSERT OR REPLACE INTO processed_books (book, max_phrase_length) VALUES (?, ?)''', (book_id, max_phrase_length))
98
+ conn.commit()
99
+ logging.info("Database population complete.")
100
+
101
+ def insert_phrase_to_db(gematria_sum, phrase_candidate, book, chapter, verse):
102
+ """Inserts a phrase and its Gematria value into the database."""
103
+ global conn
104
+ cursor = conn.cursor()
105
  try:
106
+ cursor.execute('''
107
+ INSERT INTO results (gematria_sum, words, book, chapter, verse)
108
+ VALUES (?, ?, ?, ?, ?)
109
+ ''', (gematria_sum, phrase_candidate, book, chapter, verse))
110
+ conn.commit()
111
+ logging.debug(f"Inserted phrase: {phrase_candidate} (Gematria: {gematria_sum}) at {book}:{chapter}:{verse}")
112
  except sqlite3.IntegrityError:
113
+ logging.debug(f"Phrase already exists: {phrase_candidate} (Gematria: {gematria_sum}) at {book}:{chapter}:{verse}")
 
 
114
 
115
  def get_translation(phrase):
116
+ """Retrieves or generates the English translation of a Hebrew phrase."""
117
+ global translator, conn
118
+ cursor = conn.cursor()
119
+ cursor.execute('''
120
+ SELECT translation FROM results
121
+ WHERE words = ?
122
+ ''', (phrase,))
123
+ result = cursor.fetchone()
124
+ if result and result[0]:
125
+ return result[0]
126
+ else:
127
+ translation = translate_and_store(phrase)
128
+ cursor.execute('''
129
+ UPDATE results
130
+ SET translation = ?
131
+ WHERE words = ?
132
+ ''', (translation, phrase))
133
+ conn.commit()
134
+ return translation
 
135
 
136
  def translate_and_store(phrase):
137
+ """Translates a Hebrew phrase to English using Google Translate and handles potential errors."""
138
+ global translator
139
+ max_retries = 3
140
+ retries = 0
141
+
142
+ while retries < max_retries:
143
+ try:
144
+ translation = translator.translate(phrase)
145
+ logging.debug(f"Translated phrase: {translation}")
146
+ return translation
147
+ except (exceptions.TranslationNotFound, exceptions.NotValidPayload,
148
+ exceptions.ServerException, exceptions.RequestError, requests.exceptions.ConnectionError) as e:
149
+ retries += 1
150
+ logging.warning(f"Error translating phrase '{phrase}': {e}. Retrying... ({retries}/{max_retries})")
151
+
152
+ logging.error(f"Failed to translate phrase '{phrase}' after {max_retries} retries.")
153
+ return "[Translation Error]"
154
 
155
  def search_gematria_in_db(gematria_sum):
156
+ """Searches the database for phrases with a given Gematria value."""
157
+ global conn
158
+ cursor = conn.cursor()
159
+ cursor.execute('''
160
+ SELECT words, book, chapter, verse FROM results WHERE gematria_sum = ?
161
+ ''', (gematria_sum,))
162
+ results = cursor.fetchall()
163
+ logging.debug(f"Found {len(results)} matching phrases for Gematria: {gematria_sum}")
164
+ return results
165
 
166
  def gematria_search_interface(phrase):
167
+ """The main function for the Gradio interface."""
168
+ if not phrase.strip():
169
+ return "Please enter a phrase."
170
+
171
+ global conn, book_names
172
+ conn = sqlite3.connect('gematria.db')
173
+ cursor = conn.cursor()
174
+
175
+ phrase_gematria = calculate_gematria(phrase.replace(" ", ""))
176
+ logging.info(f"Searching for phrases with Gematria: {phrase_gematria}")
177
+
178
+ matching_phrases = search_gematria_in_db(phrase_gematria)
179
+ if not matching_phrases:
180
+ return "No matching phrases found."
181
+
182
+ # Sort results by book, chapter, and verse
183
+ sorted_phrases = sorted(matching_phrases, key=lambda x: (x[1], x[2], x[3]))
184
+
185
+ # Group results by book
186
+ results_by_book = defaultdict(list)
187
+ for words, book, chapter, verse in sorted_phrases:
188
+ results_by_book[book].append((words, chapter, verse))
189
+
190
+ # Format results for display
191
+ results = []
192
+ results.append("<div class='results-container'>")
193
+ for book, phrases in results_by_book.items():
194
+ results.append(f"<h4>Book: {book_names.get(book, 'Unknown')}</h4>")
195
+ for words, chapter, verse in phrases:
196
+ translation = get_translation(words)
197
+ book_name_english = book_names.get(book, 'Unknown')
198
+ link = f"https://www.biblegateway.com/passage/?search={quote_plus(book_name_english)}+{chapter}%3A{verse}&version=CJB"
199
+ results.append(f"""
200
+ <div class='result-item'>
201
+ <p>Chapter: {chapter}, Verse: {verse}</p>
202
+ <p class='hebrew-phrase'>Hebrew Phrase: {words}</p>
203
+ <p>Translation: {translation}</p>
204
+ <a href='{link}' target='_blank' class='bible-link'>[See on Bible Gateway]</a>
205
+ </div>
206
+ """)
207
+ results.append("</div>") # Close results-container div
208
+
209
+ conn.close()
210
+
211
+ # Add CSS styling
212
+ style = """
213
+ <style>
214
+ .results-container {
215
+ display: grid;
216
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
217
+ gap: 20px;
218
+ }
219
+
220
+ .result-item {
221
+ border: 1px solid #ccc;
222
+ padding: 15px;
223
+ border-radius: 5px;
224
+ box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
225
+ }
226
+
227
+ .hebrew-phrase {
228
+ font-family: 'SBL Hebrew', 'Ezra SIL', serif;
229
+ direction: rtl;
230
+ }
231
+
232
+ .bible-link {
233
+ display: block;
234
+ margin-top: 10px;
235
+ color: #007bff;
236
+ text-decoration: none;
237
+ }
238
+ </style>
239
+ """
240
+
241
+ return style + "\n".join(results)
242
+
243
+ def flatten_text(text):
244
+ """Helper function to flatten nested lists into a single list."""
245
+ if isinstance(text, list):
246
+ return " ".join(flatten_text(item) if isinstance(item, list) else item for item in text)
247
+ return text
248
 
249
  def run_app():
250
+ """Initializes and launches the Gradio app."""
251
+ initialize_database()
252
+ initialize_translator()
253
+
254
+ # Pre-populate the database
255
+ tanach_texts = process_json_files(1, 39)
256
+ populate_database(tanach_texts, max_phrase_length=12)
257
+ tanach_texts = process_json_files(27, 27)
258
+ populate_database(tanach_texts, max_phrase_length=24)
259
+
260
+ iface = gr.Interface(
261
+ fn=gematria_search_interface,
262
+ inputs=gr.Textbox(label="Enter phrase"),
263
+ outputs=gr.HTML(label="Results"),
264
+ title="Gematria Search in Tanach",
265
+ description="Search for phrases in the Tanach that have the same Gematria value.",
266
+ live=False,
267
+ allow_flagging="never"
268
+ )
269
+ iface.launch()
 
 
 
 
 
270
 
271
  if __name__ == "__main__":
272
+ run_app()
gematria.db DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:635af9e89ddd0c0ebb0c43f05db64b2014f3ce6390cc85e9c7f51201bef214ac
3
- size 16007168
 
 
 
 
util.py CHANGED
@@ -2,8 +2,20 @@ import json
2
  import re
3
 
4
  def process_json_files(start, end):
 
 
 
 
 
 
 
 
 
 
 
 
5
  base_path = "texts"
6
- results = []
7
 
8
  for i in range(start, end + 1):
9
  file_name = f"{base_path}/{i:02}.json"
@@ -11,14 +23,14 @@ def process_json_files(start, end):
11
  with open(file_name, 'r', encoding='utf-8') as file:
12
  data = json.load(file)
13
  if data:
14
- # Return a tuple of book_id and text data
15
- results.append((i, {"title": data.get("title", "No title"), "text": data.get("text", [])}))
16
 
17
  except FileNotFoundError:
18
- results.append((i, {"error": f"File {file_name} not found."})) # Use a tuple here
19
  except json.JSONDecodeError as e:
20
- results.append((i, {"error": f"File {file_name} could not be read as JSON: {e}"})) # Use a tuple here
21
  except KeyError as e:
22
- results.append((i, {"error": f"Expected key 'text' is missing in {file_name}: {e}"})) # Use a tuple here
23
 
24
  return results
 
2
  import re
3
 
4
  def process_json_files(start, end):
5
+ """
6
+ Processes JSON files containing Tanach text and returns a dictionary
7
+ mapping book IDs to their data.
8
+
9
+ Args:
10
+ start: The starting book ID (inclusive).
11
+ end: The ending book ID (inclusive).
12
+
13
+ Returns:
14
+ A dictionary where keys are book IDs and values are dictionaries
15
+ containing 'title' and 'text' fields.
16
+ """
17
  base_path = "texts"
18
+ results = {} # Use a dictionary to store results
19
 
20
  for i in range(start, end + 1):
21
  file_name = f"{base_path}/{i:02}.json"
 
23
  with open(file_name, 'r', encoding='utf-8') as file:
24
  data = json.load(file)
25
  if data:
26
+ # Store book ID as key and book data as value
27
+ results[i] = {"title": data.get("title", "No title"), "text": data.get("text", [])}
28
 
29
  except FileNotFoundError:
30
+ logging.warning(f"File {file_name} not found.")
31
  except json.JSONDecodeError as e:
32
+ logging.warning(f"File {file_name} could not be read as JSON: {e}")
33
  except KeyError as e:
34
+ logging.warning(f"Expected key 'text' is missing in {file_name}: {e}")
35
 
36
  return results