Trước khi viết KTMT, công việc hàng ngày của chúng tôi đã từng theo một vòng lặp như sau:
1 2 3 4 5 6 7 8 |
|
Chúng tôi nhận ra có rất nhiều vấn đề ở vòng lặp này: vấn đề tìm kiếm cũng như lời giải hoàn toàn vụn vặt và thiếu tính khái quát, cùng một vấn đề hoặc vấn đề tương tự nhau nhưng nhiều khi phải google rất nhiều, nhiều khi giải quyết được vấn đề nhưng đấy lại không phải là cách giải quyết tốt nhất, cách giải quyết tốt nhất nhiều khi lại đến từ bạn bè xung quanh mình.
Chúng tôi nhận thấy nếu như không tổng hợp lại những điều mình đã tìm hiểu thì sẽ không có cách nào nhớ được cách giải quyết. Cách đơn giản nhất mà chúng tôi đã nhận thấy là viết và chia sẻ cho bạn bè. Viết giúp tổng hợp các cách giải quyết vấn đề, giúp lưu lại cách giải quyết đó cho những lần sau. Viết cũng chính là giải thích lại vấn đề cho chính bản thân sau này. Chia sẻ giúp nhận được góp ý từ những người giỏi hơn mình. Do vậy chúng tôi đã bắt đầu blog KTMT. Giống như đã viết ở KTMT blog nguồn mở, chúng tôi nhận thấy chúng tôi dần dần thoát khỏi vòng lặp nói trên.
Để hiểu rõ một điều gì đó, hãy thử giải thích điều đó một cách đơn giản nhất.
If you’re thinking without writing, you only think you’re thinking.
Khi chúng ta không viết ra, chúng ta chỉ tưởng là chúng ta đã biết thôi. Thực sự là chúng ta là chưa biết gì cả.
Hãy giải thích một cách đơn giản và hãy viết ra là thông điệp của 2 vĩ nhân trên. Do vậy chúng tôi tin tưởng viết chính là cách giải quyết cho vấn đề của chúng tôi.
Bắt đầu viết không hề đơn giản. Chúng tôi đã từng thử khảo sát và nhận ra viết lách thật sự không hề dễ. Các bạn trả lời cho điều tra trên gặp những vấn đề sau đây:
Tôi không phải là một chuyên gia về một vấn đề gì cả, vậy nên chả biết viết về cái gì cả!!
Đây có lẽ là một lý do thiếu thuyết phục nhất. Bạn không cần phải là chuyên gia mới viết được blog. Trong 85 bài viết của KTMT blog, có những chủ đề mà chúng tôi hoàn toàn chưa hiểu rõ cho đến khi bắt tay vào tìm hiểu và viết lại. Và chính nhờ việc nghiên cứu rất nhiều để viết đã giúp chúng tôi hiểu ra nhiều điều.
Nếu tôi viết một thứ gì đó không đúng, hay viết sai, tôi sẽ bị nhìn như một thằng đần trên internet
Đây có lẽ là một lý do làm nhiều bạn “sợ” viết nhất. Chúng tôi cũng như bạn, chúng tôi cũng dễ mắc phải các sai lầm. Không phải 100% kiến thức chúng tôi viết ra ngay lần đầu tiên là chính xác. Và chính các bạn, những người đọc là những người giúp chúng tôi nhận ra điều đó, trách nhiệm của chúng tôi là sửa lại cho đúng. Vậy ai là người có lợi ở đây: người viết ra, hay người không viết ra? Chắc các bạn có thể tự trả lời được câu hỏi này.
Điều quan trọng nhất tự việc nhầm lẫn (make mistake) là việc thu dọn những nhầm lẫn đó, và học những điều mới từ nó.
Tôi có thể code tốt, nhưng viết thì chịu, viết câu cú đúng ngữ pháp, có nội dung hợp lý với tôi như một cực hình.
Điều này chúng tôi hoàn toàn đông ý với bạn. Viết tốt là một trong những điều khó nhất mà tôi từng biết. Viết để cho mình hiểu đã khó, cho người khác, đặc biệt là cho những người không cùng kĩ năng với bạn hiểu được còn khó hơn. Tuy nhiên trong công việc hàng ngày, 50% việc bạn phải làm là giao tiếp, là nói cái mình hiểu cho người khác hiểu. Việc tập luyện kĩ năng viết cho người khác hiểu chính là giúp tăng kĩ năng giao tiếp của bạn lên. Hãy kiên trì và sẽ đến một lúc các bạn nhận ra rằng việc viết tốt giúp bạn nhiều đến thế nào.
Hy vọng bài viết đến đây đã truyền tải được phần nào những gì chúng tôi đang suy nghĩ về việc chia sẻ các vấn đề kĩ thuật bằng cách viết ra. Chính vì tầm quan trọng của việc chia sẻ các kiến thức kĩ thuật, và muốn phủ rộng hơn văn hoá viết ra và chia sẻ với cộng đồng kĩ thuật tại Việt Nam nói chung, chúng tôi đã quyết định làm một điều lớn hơn là chỉ open blog.
Chúng tôi đã quyết định xây dựng một nền tảng, mà ở đó ai cũng có thể viết để chia sẻ kiến thức của mình một cách dễ dàng, có thể tìm kiếm và học hỏi kiến thức có chất lượng từ những người cùng làm kĩ thuật chuyện nghiệp khác. Bạn hãy tưởng tượng đó là một kho kiến thức chất lượng cao, một môi trường cởi mở và tôn trọng lẫn nhau của những người có cùng niềm đam mê về kĩ thuật.
Nền tảng được đặt tên là Kipalog và đặt tại trang web: http://kipalog.com
Kipalog là cách gọi tắt của “keep a log”, cũng chính là khái niệm chủ đạo của nền tảng này, coi trọng việc “log” hay là giữ lại các kiến thức của bạn bằng cách “viết ra”.
Vậy bạn có thể làm gì với Kipalog:
Tại sao bạn nên bắt tay vào đăng ký và viết bài trên Kipalog:
Chúng tôi sẽ chuyển blog KTMT thành một tổ chức trên Kipalog.
Blog KTMT sẽ vẫn được giữ ở trạng thái hoạt động, nhưng sẽ không cập nhật các bài viết mới. Các bài viết mới sẽ được viết dưới tổ chức KTMT. Bạn nào muốn viết blog cho KTMT có thể tham gia tổ chức KTMT trên Kipalog cùng chúng tôi.
Chúng tôi hy vọng bạn sẽ thích Kipalog. Trên hơn cả, chúng tôi hy vọng các bạn xem Kipalog không chỉ là nơi để đọc, mà còn là nơi các bạn tích cực chia sẻ vốn kiến thức của bản thân.
Cám ơn các bạn. Đón đọc những tri thức của bạn tại kipalog.com :)
]]>“If you’re thinking without writing, you only think you’re thinking.” – Lessie Lamport
Mình rất thích câu nói trên của Lamport. Đối với mình câu nói trên hay ở chỗ: vế sau của câu là một cách nói đệ quy. “Bạn chỉ nghĩ rằng là bạn đang suy nghĩ”. Suy nghĩ là để tìm ra câu trả lời cho một câu hỏi nào đó. Nhưng nếu không có việc viết lách, đối tượng của suy nghĩ sẽ chỉ đơn thuần là “sự suy nghĩ” - và chúc mừng bạn, bạn rơi vào vòng lặp đệ quy vô hạn. Cách duy nhất để bạn thoát khỏi vòng lặp đệ quy vô hạn của suy nghĩ là phải định nghĩa một điểm khởi đầu trong tư duy. Viết lách chính là điểm khởi đầu đó.
Blog ktmt cũng bắt đầu với một ý tuởng đơn giản như ở trên: một chỗ để các thành viên viết và chia sẻ cho những thành viên khác điều mình học đuợc. Bài viết không cần phức tạp, chỉ cần có nội dung liên quan đến kỹ thuật là được chấp nhận. “Muốn chia sẻ điều mình học đuợc?” - Hãy viết một bài về chủ đề đó và các thành viên khác sẽ đọc. Ý tuởng thì là vậy nhưng thực hiện đuợc nó thật sự rất gian nan. Khi thực sự viết ra, bọn mình mới thấy để viết đuợc một bài cần đầu tư rất nhiều thời gian, từ nghiên cứu cho đến viết code mẫu để demo cho bài viết. Tuy vậy nhờ việc viết ra những điều mình tuởng là biết bọn mình khám phá ra có rất nhiều chỗ bọn mình chưa thực sự biết như mình tuởng. Mỗi câu văn khó hiểu cho người đọc thật sự không chỉ do cách viết mà còn do sự thiếu tuờng tận và am hiểu về chủ đề đuợc viết. Cứ mỗi bài viết, bọn mình là đọc rất kỹ, kiểm tra cho nhau và đưa ra những góp ý sửa chữa cho nhau. Nhờ vậy am hiểu của ngưòi viết về chủ đề đang viết cũng như nguời đọc trở nên sâu sắc hơn.
Bọn mình đưa bài lên một blog chỉ đơn giản với mục đích cho các thành viên khác đọc và góp ý. Thế nhưng dần dần bọn mình nhân ra rằng không chỉ cá nhân nhóm tác giả mà còn rất nhiều bạn đọc khác cũng đọc và góp ý. Bài viết của bọn mình một phần nào đó đã đem lại ích lợi cho cho cộng đồng. Ngoài ra bọn mình cũng nhận được nhiều đóng góp cũng như câu hỏi từ bạn đọc. Những đóng góp và câu hỏi giúp bọn mình rất nhiều trong việc hiểu sâu sắc hơn chủ đề mỗi bài viết. Bọn mình nhận thấy bọn mình không những học đuợc lẫn nhau mà còn học đuợc từ rất nhiều bạn đọc khác - điều bọn mình rất vui.
Gần đây bọn mình nhận đuợc nhiều ý kiến đề xuất đóng góp bài viết. “Sao không!” là câu nói đầu tiên xuất hiện trong đầu của tất cả các thành viên blog ktmt. Đối với bọn mình đây là điều không gì vui hơn. Blog sẽ có nhiều bài viết mới từ nhiều góc độ suy nghĩ cũng như kiến thức hơn (và bọn mình sẽ có cơ hội học được từ nhiều ngưòi hơn). Các bạn tác giả sẽ có nơi để luyện tập viết lách. Độc giả sẽ có nhiều bài viết để đọc hơn. Sau khi cân nhắc nhiều yếu tố - hầu hết chỉ có lợi - bọn mình đi đến quyết định:
Biến ktmt trở thành một blog cộng đồng!
Blog ktmt sẽ không chỉ bao gồm bài viết của nhóm tác giả hiện tại mà sẽ nhận bài viết từ tất cả các bạn nào muốn đóng góp. Mỗi bài viết của KTMT đều đuợc viết bằng ngữ pháp Markdown, sử dụng Octopress để biên tập và chia sẻ trên máy chủ của github. Về cách sử dụng các tool này các bạn có thể tham khảo: Blogging With Github and Octopress. Để giữ blog không thay đổi quá nhiều ở giai đoạn đầu, bọn mình quyết định quy trình đóng góp bài viết như sau:
Tiêu chí biên tập là điều bọn mình đã suy nghĩ nhưng vẫn chưa tìm ra đuợc những tiêu chí xác đáng. Vì vậy truớc mắt bọn mình tạm đề ra các tiêu chí biên tập như duới đây:
Ngoài ra bọn mình cũng đã thiết lập một trang Facebook group của blog. Các thảo luận xung quanh bài viết sẽ đuợc thực hiện thông qua page này.
Trước mắt ban biên tập sẽ chỉ bao gốm những thành viên đóng góp thuờng xuyên của KTMT:
Trong tuơng lai bọn mình muốn sẽ có những thành viên xuất sắc trong biên tập. Vì vậy bọn mình tạm đề ra một tiêu chí để tham gia ban biên tập như sau:
“Là một thành viên đóng góp thuờng xuyên và có nhiều bài viết tốt cho blog KTMT”.
Thành viên nào đóng góp đuợc 5 bài viết sẽ đuợc tính là thành viên thường xuyên. Về tiêu chí bài viết tốt, bọn mình hiện đang trong quá trình xây dựng cách đánh giá cho tiêu chí này.
Bọn mình mong chờ nhiều bài viết tốt hơn nữa từ các bạn!
Chúng ta hiểu laziness như thế naò? Lazy evaluation
nghĩa là, việc evaluate các đối số của hàm sẽ đuợc trì hoãn càng lâu càng tốt, chúng sẽ không đuợc evaluate cho đến khi biểu thức thực sự cần đến gía trị của chúng. Khi một biểu thức đuợc đưa làm đối số cho một hàm, nó chỉ đuợc đóng gọn lại thành một biểu thức chưa đuợc đánh gía (unevaluated expression), mà chưa đuợc tính toán gì cả.
Chúng ta sẽ sử dụng Lecture 7 của course CIS 194 (Link) để minh họa cho việc tìm hiểu Laziness. Bên cạnh đó, tôi sẽ trình bày qua những syntax cơ bản của Haskell theo bài viết.
Trong bài tập thứ nhất, chúng ta sẽ viết một hàm fib
để tính ra số Fibonacci thứ n
1 2 3 4 5 |
|
Bên trên là điển hình của cách viết một hàm (function) trong Haskell. Truớc tiên, chúng ta có tên hàm nằm truớc dấu ::
, trong truờng hợp này là hàm fib
. Tiếp theo, nằm sau dấu ::
, chúng ta có Integer -> Integer
, đây chính là nơi khai báo đối số và giá trị trả về của Haskell. Trong truờng hợp này, hàm fib
nhận đối số là Integer (số Fibonacci thứ mấy) và trả về Integer (số Fibonacci cần tìm). Tài liệu sau đây nói một cách khá đầy đủ về syntax viết function, bạn nên đọc thêm: Syntax in Functions
Sau dòng khai báo tên hàm và nguyên mẫu hàm là định nghĩa của một loạt pattern matching. Haskell sẽ match từ trên xuống duới, do vậy truớc tiên ta khai báo base case cho số Fibonacci thứ 0 và thứ 1. Các số Fibonacci tiếp theo, vì không match các base pattern nên sẽ sử dụng pattern thứ ba, cộng hai số Fibonacci phía truớc.
Chúng ta thấy cách khai báo pattern matching khá gần gũi vớí định nghiã dãy Fibonacci chúng ta đã học. Tiếp theo, ta định nghiã một dãy Fibonacci vô hạn như sau:
1 2 |
|
map
là một function trong Haskell, nhận vaò hai đối số là một hàm f và một danh sách. Nó sẽ trả về một danh sách mới mà các phần từ là kết qủa trả về tuơng ứng khi áp dụng hàm f với từng phần tử trong danh sách cũ. [0..]
là cách viết danh sách tất cả các số nguyên duơng. Như vậy, ta có fibs1
là danh sách tất cả các số Fibonacci.
1 2 3 4 5 6 |
|
Bạn sẽ thấy một vài số Fibonacci đầu tiên hiện ra rất nhanh, nhưng đến một lúc nào đó, sẽ rất lâu để tính đuợc số Fibonacci tiếp theo. Lý do là vì mỗi số Fibonacci bị tính đi tính lại quá nhiều lần!
Và đây là lúc chúng ta áp dụng Laziness của Haskell.
1 2 3 |
|
Chúng ta đã biết dãy Fibonacci bắt đầu từ hai số 0, 1, nên ta khởi đầu khai baó dãy bằng 0,1.
Giả sử ta đã có một dãy Fibonacci vô hạn:
1
|
|
tail
là phép toán bỏ đi phần tử đầu tiên của một dãy:
1
|
|
zipWith
kết hợp 2 dãy trên theo từng cặp phần tử sử dụng một phép toán naò đó (ở đây là phép cộng):
1 2 3 4 5 |
|
Như vậy, dãy vô hạn Fibonacci có thể đuợc tính bằng cách thêm hai phần tử đầu tiên (0, 1) vào kết quả sau khi zip dãy Fibonacci vô hạn với tail
của nó!
Và đây chính là sức mạnh của laziness! fibs2
là kết quả trả về mà chúng ta lại có thể để xuất hiện ở bên vế phải! Haskell chỉ evaluate vế phải khi naò thực sự cần thiết, cho nên mỗi khi cần tính một phần tử mới cho dãy fibs2
, nó mới quay ra tìm lại những phần tử đằng truớc (đã đuợc tính).
fibs2
với GHCi và quan sát, số Fibonacci tuôn ra như thác lũ :) Hãy thử chỉ lấy 100 số Fibonacci ban đầu xem:
1
|
|
100 số Fibonacci đầu tiên xuất hiện trong chớp mắt!
Trong phần này, chúng ta định nghĩa kiểu dữ liệu (data type) Stream
, gần giống với list
nhưng bị ràng buộc là phải vô hạn
1
|
|
Bên trên là cách khai baó một kiểu dữ liệu, bắt đầu bằng từ khoá data
. Stream
là tên kiểu dữ liệu, tiếp sau đó đến truớc dấu =
là type variable
, tức là a
có thể thay cho Integer
, String
, … và ta có kiểu dữ liệu Stream Integer
hoặc Stream String
tuơng ứng. Phiá sau dấu =
là constructor của kiểu dữ liệu này: Cons
, với 2 đối số thuộc kiểu a
và Stream a
. Bạn hãy tham khaỏ về cách taọ kiểu dữ liệu tại Link này
Hàm sau convert một stream thành list:
1 2 |
|
Trong exercise 4, chúng ta phải viết một instance của Show
cho kiểu dữ liệu Stream. đến đây, chúng ta cần biết thêm về khái niệm Typeclass
của Haskell. Typeclass có thể coi là một loại “giao diện”, nó định nghĩa một số loại hành vi, và các kiểu dữ liệu mà có cùng hành vi đó có thể được khai báo là instance của typeclass đó. Lấy ví dụ, Eq
typeclass định nghĩa một giao diện cho những thứ có thể so sánh được. Cụ thể các hành vi mà nó định nghĩa như sau:
1 2 3 4 5 |
|
Theo đó, bất cứ type nào muốn được là một instance của Eq
typeclass sẽ phải khai báo các hàm (==)
, (/=)
(trên thực tế, chỉ cần khai báo một hàm, vì hàm còn lại được định nghĩa là phủ định của hàm kia).
Ví dụ, ta có một type như sau:
1
|
|
Chúng ta muốn Coin
type là một instance của Eq
typeclass thì ta làm như sau:
1 2 3 4 |
|
Ở đây, chúng ta vẫn dùng pattern matching để định nghĩa hàm (==) với từng trường hợp của 2 đối số.
Quay trở lại với exercise 4, chúng ta định nghĩa Stream a
thành một instance của Show
typeclass như sau:
1 2 |
|
Chúng ta thấy sự xuất hiện của kí tự “lạ”: =>
. Trong khai báo instance, những gì xuất hiện trước dấu =>
là những ràng buộc về type. Ở đây, chúng ta muốn Stream a
là instance của Show
thì bản thân a
, một type variable, cũng phải là một instance của Show
. Có như vậy, chúng ta mới định nghĩa được hàm show
của Show
typeclass cho Stream a
dựa trên hàm show
cho a
.
Đến kí tự lạ tiếp theo: $
. Đây chỉ đơn giản là một chỉ dẫn cho Haskell là tất cả những gì xuất hiện sau $
có độ ưu tiên phép toán cao hơn, tức là show $ take 20 (streamToList s)
tương đương với show (take 20 (streamToList s))
. Nhưng chúng ta không muốn dùng quá nhiều dấu ngoặc, phải không :)
Trong định nghĩa hàm show
cho Stream a
, chúng ta không muốn nó in hết ra vô hạn phần tử, mà chỉ muốn in ra 20 phần tử đầu tiên. Do đó chúng chuyển nó thành list bằng hàm streamToList
, lấy 20 phần tử đầu tiên bằng take 20
và áp dụng hàm show
cho List a
type (khi a
là instance của Show
rồi thì [a]
cũng là instance của Show
)
Chúng ta phải có một vài Stream
để test. Hãy định nghĩa chúng:
1 2 3 4 5 6 7 8 |
|
streamRepeat
tạo ra một stream chứa vô hạn các phần tử giống hệt nhau. streamMap
áp dụng một hàm (a->b)
lên tất cả các phần tử của một Stream a
để nhận được một Stream mới: Stream b
. Cuối cùng streamFromSeed
là một cách khác để tạo ra một stream, bằng cách bắt đầu từ một “hạt giống” thuộc type a
, cũng chính là phần tử đầu tiên của Stream, rồi liên tục sử dụng một hàm có kiểu a->a
để tạo ra các phẩn tử tiếp theo.
Tôi sẽ để dành phần này cho độc giả chiêm nghiệm xem tại sao lại viết như vậy.
Bây giờ, chúng ta hãy cùng test thử những gì chúng ta đã viết trong GHCi
1 2 3 4 5 6 7 8 |
|
Như vậy ta đã có một số hàm để làm việc với Stream
, ta sẽ thử định nghĩa dãy số tự nhiên bằng Stream
như sau:
1 2 |
|
1 2 |
|
Ví dụ tiếp theo: Tính ruler function f(n) = số mũ lớn nhất của 2 là ước số của n. Bình thuờng chúng ta sẽ nghĩ đến việc chia 2 cho đến khi lẻ thì thôi. Nhưng tôi sẽ trình bày một lời giải khác, sử dụng laziness và cấu trúc dữ liệu vô hạn của Haskell.
Thay vì tính từng f(n) một, chúng ta sẽ xây dựng hẳn cả dãy số ruler
f(n) với n bắt đầu từ 1: 0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4...
, trong đó phần tử thứ n trong stream là số mũ lớn nhất của 2 là ước số của n.
Dễ thấy có thể coi dãy trên là trộn xen kẽ của hai dãy, một dãy toàn 0 (vì tuơng ưng với n lẻ nên số mũ lớn nhất của 2 chỉ là 0) và dãy còn lại tuơng ứng với n chẵn. điều kì diệu là dãy thứ hai bao gồm chính các phần tử của dãy ruler
nhưng cộng thêm 1 (Bạn thử suy nghĩ xem tại sao). Vì thế mà ta có cách khai báo ruler
là một Stream Integer
rất đẹp như sau:
1 2 3 4 5 |
|
Lại thêm một kí tự lạ: tilde sign ~
! Tôi đã thêm nó vào truớc đối số thứ hai của interleaveStreams
. Kí tự này dùng để báo hiệu compiler đừng evaluate đối số thứ hai này. Nếu không có kí tự ~
, pattern matching của hàm interleaveStream
sẽ phải evaluate đối số thứ hai để đảm bảo nó thuộc type Stream a
. đó không phải là điều chúng ta muốn, vì hàm ruler
gọi hàm interleaveStream
với đối số thứ hai chứa ruler
, tức là gọi đệ quy vô hạn lần. Nếu đối số thứ hai của interleaveStream
không lazy, hàm này sẽ dừng mãi ở việc evaluate để phục vụ pattern matching.
Nói nôm na, thêm dấu ~
truớc một đối số là chúng ta đã bảo với compiler là: “đừng lo, tôi đảm bảo đối số này sẽ có kiểu Stream a, nên đừng evaluate làm gì” :)
Bài viết này đã trình bày một số ví dụ để minh họa lazy evaluation của Haskell. Nó cho phép ta làm việc với những kiểu cấu trúc dữ liệu vô hạn, một pattern khá thuờng gặp trong Haskell. Việc định nghiã một cấu trúc dữ liệu vô hạn thực chất chỉ taọ ra một biểu thức chưa đuợc evaluate, mà ta sử dụng nó để chỉ ra cấu trúc dữ liệu hoàn chỉnh “có thể” phát triển đến như thế naò, và chỉ phần naò cần thiết mới đuợc tính toán.
Tuy nhiên, chủ đề laziness là một chủ đề khá phức tạp, đặc biệt khi chúng ta muốn đánh giá time và space của program. Có khá nhiều bài viết trên mạng về vấn đề này, một trong số đó bạn có thể tham khảo thêm là:
]]>Đầu tiên tôi sẽ nói về các loại hình quảng cáo trên internet.
Đây có lẽ là loại hình quảng cáo dễ hiểu nhất và mặc dù xuất hiện từ thời ‘xa xưa’, nhưng loại hình này vẫn khá thông dụng cho đến hiện nay. Cách quảng cáo này thông qua việc liên lạc trực tiếp giữa ‘người bán’ và ‘người mua’ (người mua ở đây có thể là nhà quảng cáo (advertiser) hoặc cũng có thể là công ty trung gian (ad-agency)). Hình thức này thường có giới hạn thời gian với một cái giá cố định, ví dụ bạn muốn đăng banner trên vnexpress trong vòng một ngày, bạn liên lạc trực tiếp cho vnexpress, trả cho vnexpress một cái giá cố định, hoặc là trả theo ‘Impression’ (hay là số view). Hình thức quảng cáo này thường có giá trị về mặt ‘brand’, nên thường khách hàng sẽ là các nhãn hiệu lớn, muốn cho khách hàng nắm được thông tin về ‘campaign’ của mình, hoặc để cho hình ảnh ‘brand’ in đậm vào tâm trí người nhìn. Hình thức này có ưu điểm và nhược điểm là :
Đây là cách quảng cáo đem lại doanh thu chủ yếu cho các search engine thông dụng như google hay yahoo. Để hình dung về hình thức này, bạn chỉ cần tham khảo 2 ví dụ dưới đây mà mình chụp lại của google.
Ở hình thứ nhất, khi tôi search từ khoá liên quan đến quần áo, google sẽ đưa quảng cáo liên quan đến từ khoá đó lên đầu. Để mua được quảng cáo loại này, thì advertiser phải mua quảng cáo dưới dạng từ khoá (keyword). Dạng quảng cáo này chính là hệ thống Adwords nổi tiếng của google mà chắc bạn đã từng nghe qua.
Ở hình thứ 2, bạn có thể thấy khi search từ khoá liên quan đến nhãn hiệu hàng hoá montbell, google không chỉ đưa ra link dẫn đến trang web có món hàng, mà còn đưa ra cụ thể chi tiết của từng sản phẩm. Hình thức quảng cáo này vẫn dựa trên nên tảng là adwords (mua keywords), tuy nhiên ở một mức cao hơn gọi là listing ads. Để làm được việc này thì advertisers phải cung cấp cho google thông tin về sản phẩm (link ảnh, giá cả…) dựa trên hình thức feed (bạn có thể hình dung giống như RSS, advertiser cung cấp http://advertiser.com/feed.xml, google fetch thông tin về, đưa vào cơ sở dữ liệu của google).
Hình thức quảng cáo dựa trên search engine có ưu điểm và nhược điểm là:
Loại hình quảng cáo này là loại hình thông dụng và ‘bình dân’ nhất, với đặc điểm chính là ‘tiến hoá’ từ quảng cáo thuần (pure advertisement). Pure advertisement có nhược điểm là phải có quá trình trao đổi trực tiếp giữa ‘người mua’ và ‘người bán’, và người bán ở đây thường chỉ là các media lớn, có lượng impression cực cao. Các media lớn này thường là các trang web báo chí (The NYTimes, Vnexpress…), hay các trang portal (như yahoo news..). Vậy các media nhỏ hơn làm sao để có thể bán inventory của mình (inventory là các ‘chỗ trống’ trên trang web để có thể đặt quảng cáo vào). Đây chính là vấn đề mà display advertisement-programmatic giải quyết, khi sinh ra các khái niệm mà tôi đã đề cập ở bài viết trước như : Ad-network, Ad-exchange, DSP, SSP…
Ad-network lớn nhất hiện nay có thể nhắc đến Google Display Network (GDN). GDN bao gồm cả hệ thống google adsense mà các bạn nào đã từng đặt quảng cáo adsense có thể đã biết. Khi đặt quảng cáo adsense thì bạn (media) đã gia nhập vào hệ thống network của google (GDN).
Mô hình quảng cáo này sẽ được tôi nhắc đến kĩ hơn trong bài viết sắp tới, tuy nhiên có thể lại các ưu nhược điểm như sau:
Ngoài các hình thức trên thì hiện nay còn rất nhiều hình thức quảng cáo khác ‘mới nổi’ mà có thể kể đến điển hình như:
Ở bài viết này tôi đã giới thiệu một cách cơ bản nhất về các loại hình quảng cáo. Bài viết chủ yếu đi về các khái niệm, để làm nền tảng cho bài viết sắp tới tôi sẽ nói rõ hơn về khía cạnh kĩ thuật của hình thức quảng cáo hiển thị (display advertisement), cũng như các cách để ‘tracking’ người dùng.
]]>“Đem sản phẩm đến với cộng đồng” luôn là phần khó đối với một người làm kỹ thuật.
Chúng ta có thể say mê, thức ngày thức đêm để viết lib, viết tool, hoàn thành đoạn code mà bản thân cảm thấy tâm đắc. Tuy nhiên mọi chuyện thường kết thúc ở đó. Thiếu quảng bá và truyền thông sẽ khiến sản phẩm chỉ loanh quanh ở mức bạn bè dùng thử rồi bạn bè cho ý kiến, sản phẩm làm ra chẳng bao giờ thực sự đến được với cộng đồng.
Xây dựng xong Rainbow Stream. Tôi đã bắt đầu đi học hỏi và tìm cách quảng bá, giới thiệu. Bỏ bên lề những vấn đề kỹ thuật đã trình bày trong phần 1, phần 2 này sẽ tập trung nói về quá trình quảng bá và giới thiệu đó.
Người dùng của sản phẩm mã nguồn mở được xác định sẽ là chính những hacker như bản thân tác giả. Bước đầu tiên tôi đã thực hiện là tìm hiểu xem cộng đồng hacker trên thế giới đang đọc gì ? ở đâu ? Bản danh sách tóm tắt dưới đây sẽ đưa ra những câu trả lời theo thứ tự.
Những kênh ở phần trước có thể đem lại cho bạn lượng view đầu vào rất lớn, nhưng để user nán lại và đọc hết, dùng thử sản phẩm và tốt hơn nữa là đi giới thiệu lại sản phẩm hộ bạn, bạn cần chuẩn bị thật tốt những chỉ mục sau đây
Một cái tên dễ nhớ, dễ đánh vần, và quan trọng hơn là chưa thuộc về một sản phẩm khác là điều đầu tiên cần thiết. Hãy dùng Chrome incognito mode để kiểm tra mọi ý tưởng.
Hoặc Bitbucket hay Google Code. Tuy nhiên cộng đồng trên Github đang phát triển mạnh mẽ và nhanh chóng hơn cả. Host source code trên Github sẽ nhận được nhiều feedback và Pull Request.
Thực tế Github Project đã có thể làm official homepage. Tuy nhiên tôi vẫn mua thêm domain rainbowstream.org, làm một cái Github Page, lấy một cái theme đẹp và trỏ vào domain làm homepage thứ 2. Tại sao ư ? 2 link homepage sẽ đem lại 2 cơ hội giới thiệu trên Hacker News. Theme đẹp cho một trang homepage đơn giản có thể tìm thấy ở HTML5up.
README.md là cái đầu tiên đập vào mắt user khi họ đến homepage. Một file README tốt sẽ nói với user :
Chưa cần nói đến chất lượng sản phẩm, source code được viết test sẽ có độ tin tưởng khác hẳn với những source code không có. Đặc biệt nếu project của bạn là một library với mục đích được dùng lại, không có testcase là đồng nghĩa với không có người dùng.
Đối với cộng đồng hacker thì Twitter gần như trở thành định danh chính thức vậy. Twitter dùng để giao tiếp với user, phát đi thông báo nâng cấp hay tương tác với các tài khoản của người nổi tiếng.
Xác định được các tài khoản Twitter nổi tiếng là bước quan trọng đầu tiên. Bạn cần tìm ra keyword thể hiện được lĩnh vực mà sản phẩm của bạn đang hướng đến, lên FollowerWork và tìm kiếm account của các blogger xếp theo số lượng follower. Tìm được profile rồi, hãy google ra homepage của họ (hoặc của trang tin kỹ thuật), mở phần contact để lấy email. Bước tiếp theo là soạn một email để nhờ họ giới thiệu về sản phẩm của bạn. Hãy nhớ, nội dung phải lịch sự và nhã nhặn, tóm gọn được sơ lược về sản phẩm và có đường link đến homepage. Đừng tở ra ngại ngùng ở tư thế người đi nhờ. Bản thân các blogger cũng cần những sản phẩm hay để thu hút view cho trang web của chính họ.
Đến khi sản phẩm được nhắc đến trong 1 tweet hoặc được viết hẳn thành 1 bài trên trang tin thì bạn sẽ ngạc nhiên với độ lan truyền của nó :D
Nếu bạn dồn đủ số star tăng xấp xỉ 40~50 trong 1 ngày, Github sẽ liệt kê sản phẩm của bạn vào Trending list, là nơi rất nhiều các newsletter nhìn vào để gửi mail cho user của họ. Python Weekly sẽ nhìn Python Github Trending, Ruby Weekly sẽ nhìn Ruby Github Trending. Tương tự đối với IOS hay Android.
Tôi không biết chính xác tên của các newsletter nhiều người đọc nhất, nhưng biết chắc chắn tất cả đều lấy trên trang trending của Github. Hãy cố gắng giữ được độ tăng đều trong thời gian càng lâu, độ phủ sóng của sản phẩm lên các newsletter sẽ càng lớn.
Giới thiệu sản phẩm mã nguồn mở trên Reddit là cách dễ dàng ở mức tiếp theo. Reddit được chia thành rất nhiều các subreddit(kênh nhỏ) về các chủ đề cụ thể khác nhau. Bạn cần tìm kênh đông thành viên và liên quan đến chủ đề của sản phẩm. Sau đây là danh sách các subreddit tôi đã thử với Rainbow Stream
Muốn tin đăng trông bắt mắt và gây ấn tượng với người đọc, bạn nên post kèm 1 ảnh được up thông qua Imgur. Một tin được tỉ lệ upvote cao (>70%) có thể ở nguyên trên frontpage của subreddit khoảng vài ngày.
Chúng ta sẽ nói về phần khó nhất. Đã có vô số bài nghiên cứu về traffic của Hacker News, thời gian nào up lên là tốt nhất, chiến thuật lên front, bám front (lên được trang chủ và trụ lại trang chủ) trong thời gian dài. Bất kỳ ai cũng có thể post link lên Hacker News, tuy nhiên để post link có phần đầu là [Show HN] thì đòi hỏi phải có account đăng ký được một thời gian. Tôi đã lập account và đọc Hacker News trước khi làm Rainbow Stream, nên may mắn có thể gửi link luôn khi hoàn thành sản phẩm.
Lên front được khoảng 20 tiếng đã đem lại 5000~6000 view và ít nhất 300+ star trên Github, rất ấn tượng phải không :) Sau đây là một vài lời khuyên nhỏ
Bài viết này đã tổng kết các kinh nghiệm quảng bá của một sản phẩm mã nguồn mở đối với cộng đồng hacker trên thế giới. Mặc dù chúng ta có thể áp dụng rất nhiều các mẹo vặt, hướng dẫn để tăng view cho sản phẩm, nhưng bạn hãy nhớ, điều cốt yếu nhất vẫn là chất lượng sản phẩm của bạn.
Chất lượng bao gồm cả chất lượng trong quá trình làm ra và chất lượng support trong cả thời gian phát triển. Hãy cố gắng fix hết bug, trả lời mọi câu hỏi và review mọi Pull Request một cách cẩn thận. Bạn sẽ nhận lại không chỉ sự tôn trọng trong giới hacker mà cả những cơ hội từ trước đến nay chưa bao giờ nghĩ tới. Happy hacking!
]]>Năm mới chắc hẳn hầu hết mọi người đều có resolution của riêng mình. Một trong những resolution của tôi năm nay là học một ngôn ngữ lập trình mới, đến level có thể viết một chương trình không đơn giản với nó. (Bạn có thể tham khảo một danh sách các resolutions cho programmer ở đây). Ngôn ngữ mà tôi chọn là Haskell, một functional programming language, vì những ý tưởng trong ngôn ngữ lập trình này khác hẳn những ngôn ngữ lập trình tôi đã tiếp cận, như C/C++, Java, Python.
Tôi mới bắt đầu với Haskell được một vài tháng, và cảm thấy khá thích thú về những ý tưởng mới mẻ của nó. Tôi sẽ bắt đầu viết chia sẻ những kiến thức tôi thu thập được trong quá trình tìm hiểu ngôn ngữ này trên ktmt. Tuy nhiên, tôi phải nói trước là Haskell khá trừu tượng, và với một beginner như tôi, việc cố diễn giải các khái niệm khó của Haskell có thể sẽ không chính xác và dễ gây hiểu lầm. Vì thế, tôi sẽ tập trung vào viết những đoạn code để giải quyết một vấn đề to hoặc nhỏ nào đó và cố giải thích chúng làm được như thế bằng cách nào. Những khái niệm khó, tôi sẽ dẫn về những bài viết nổi tiếng để bạn đọc có thể tìm hiểu thêm.
Trong bài viết đầu tiên này, tôi sẽ hướng dẫn cách cài đặt những thành phần cơ bản để chúng ta có thể bắt đầu lập trình với Haskell. Cuối bài, sẽ có một chương trình Hello World rất đơn giản để chúng ta test xem môi trường của chúng ta đã hoàn thiện chưa.
Chú ý: Hiện tại tôi test trên máy tính của tôi (Windows 7). Tôi đã từng cài đặt môi trường trên Mac OS X và Ubuntu Linux, nhưng chưa có thời gian kiểm tra lại. Tôi sẽ thêm thông tin nếu cần thiết.
Đây là cách đơn giản nhất để chúng ta có thể bắt đầu với Haskell. Trên Homepage, Haskell Platform được gọi là “Haskell: batteries included”. Trên homepage này, bạn có thể tải về cả package để cài đặt cho cả ba môi trường: Windows, Linux, Mac OS X. Sau khi cài đặt, bạn sẽ có rất nhiều thành phần tiêu chuẩn để bắt đầu lập trình với Haskell (List). Trong số đó, có những thành phần tiêu biểu sau:
Đây là compiler cho Haskell.
Đây là GHC interactive interpreter. Nếu bạn đã từng lập trình với Python hoặc Ruby, bạn có thể coi GHCi giống như khi bạn gõ python
(với Python) hoặc irb
(với Ruby) trên command line. GHCi cực kì hữu dụng trong quá trình bạn viết code Haskell.
Bạn sẽ dành khá nhiều thời gian trong GHCi, nên chúng ta sẽ dành chút thời gian để config GHCi sao cho thuận tiện nhất. File config của GHCi là .ghci
, vị trí của file này tùy thuộc vào hệ thống bạn đang sử dụng, bạn tham khảo ở đây: GHCi dot files. Tôi tạo mới một file .ghci
trong Home folder với nội dung:
1
|
|
Làm vậy, mỗi lần mở GHCi, prompt của bạn chỉ đơn giản là h>
chứ không phải là tên tất cả các module đã load (bạn hãy thử xem khác nhau như thế nào nếu không có dòng setting trên)
Cabal là viết tắt của Common Architecture for Building Applications and Libraries
. Về chức năng, nó tương tự như pip
của Python hay gem
của Ruby, dùng để cài đặt những package chuẩn từ Hackage
(Haskell Central package archive). Những người viết package sử dụng Hackage
để publish các libraries hay programs của họ, và những Haskell programmer khác sử dụng các tool như cabal-install
để download và cài đặt các package này.
Trên Windows 7, mỗi khi sử dụng cabal, các package sẽ được install vào $HOME$\AppData\Roaming\cabal
. Trên Unix-based system, chúng được install vào ~/.cabal/
.
Sau đây là những thao tác đầu tiên bạn nên làm với cabal
:
1
|
|
Dùng để update list các package phiên bản mới nhất trên hackage.haskell.org
.
1
|
|
Dùng để update phiên bản cabal-install
mới nhất
Bạn có thể dùng bất cứ một text editor nào để viết code Haskell. Nếu bạn sử dụng Sublime Text, bạn nên cài plugin SublimeHaskell. Đây là plugin hỗ trợ Cabal build, error and warning highlighting, smart completion và tích hợp ghc-mod. Bạn có thể tham khảo thêm thông tin tại GitHub repo
Với Haskell, sandbox
cho phép chúng ta build các package một cách độc lập với package environment của hệ thống, bằng cách tạo ra một package environment riêng cho project hiện tại. Nếu bạn đã quen thuộc với Python’s virtualenv
hoặc Ruby’s RVM
, sandbox
là một khái niệm tương tự.
Bài viết này trình bày khá dễ hiểu về tại sao nên sử dụng sandbox
để tránh dependency hell, bạn có thể tham khảo thêm. Một số thao tác cơ bản với cabal sandbox gồm có:
1 2 3 4 |
|
Chú ý ở đây, cabal sandbox là một feature của cabal từ version 1.18 trở đi, cho nên sau khi init một sandbox hoàn toàn mới, các command tiếp theo (như build
hay install
) đều sẽ sử dụng sandbox chứ không phải package environment của hệ thống.
Chúng ta sẽ bắt đầu với ví dụ muôn thuở khi bắt đầu ngôn ngữ lập trình mới: In ra màn hình consle dòng chữ Hello World
.
1 2 3 |
|
cabal init
sẽ giúp chúng ta thêm thông tin cho project của mình, như: tên project, version, người phát triển, license,…
Tiếp theo, chúng ta edit file Cabal. Ví dụ sau khi edit, file haskell-hello-world.cabal
của tôi có nội dung như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Một số điểm lưu ý từ file cabal trên:
hs-source-dirs
là thư mục src
để Cabal biết nơi lưu các file modulesmain-is
thành Main.hs
để compiler biết đầu là main function của file binary build raghc-options
đặt thành -Wall
để chúng ta có thể thấy các Warning từ GHCbuild-depends
là nơi khai báo các library cần sử dụng, có thể kèm theo các option về yêu cầu version.File code của chúng ta nằm ở src/Main.hs
có nội dung như sau:
1 2 3 |
|
Đây là một module rất đơn giản, và vì nó được dùng làm target cho main-is
, nó phải có một function tên main
và tên của module cũng phải tên là Main
. Hiện tại, bạn chưa cần để ý vội đến cú pháp mà chỉ cần biết rằng chương trình in ra màn hình dòng chữ “Hello World!”. (Chú ý: Có thể bạn sẽ nghĩ putStrLn
tương tự như printf
hay cout
ở C/C++, nhưng với Haskell, nguyên lý sẽ khác hơn so với bạn nghĩ, nhưng đó là câu chuyện ở những bài viết sau này.)
Tiếp theo, chúng ta tạo một Cabal sandbox để chứa toàn bộ các dependencies (ví dụ Hello World này của tôi hơi trivial, vì không dùng library nào cả, nhưng vì tính đầy đủ, tôi vẫn xin trình bày về sandbox ở đây):
1
|
|
Sau bước này, chúng ta sẽ có file cabal.sandbox.config
chứa thông tin về package environment, và sandbox nằm ở thư mục .cabal-sandbox
1 2 |
|
Nếu build thành công, bạn sẽ có file binary haskell-hello-world
ở dist/build/haskell-hello-world
. Thử chạy nó xem sao:
1 2 |
|
Và chúng ta đã build thành công program đầu tiên!
Bài viết này là bài viết đầu tiên của tôi về Haskell. Chưa có gì nhiều về syntax, idea, concept, mà chỉ là những setup ban đầu để dễ dàng bắt đầu với Haskell. Trong những bài viết tiếp theo, tôi sẽ cố gắng từng bước một trình bày các ý tưởng của Haskell một cách dễ hiểu.
Hẹn gặp lại!
]]>2h sáng. “beep, you’ve got mail”. Mail từ hệ thống giám sát zabbix.
1 URL quan trọng trong hệ thống web không hiển thị được. Truy cập vào URL đó nhận status code http trả về 503. Zabbix định kỳ kiểm tra mã lỗi và khi mã trả về khác 200, zabbix gửi mail cho hắn.
“Lại có vấn đề gì rồi đây…” — hắn vùng dậy, mở laptop lên, mở browser ra và truy cập thử vào URL được thông báo. “Quả nhiên là không vào được”, hắn nghĩ. Ssh thử vào một máy chủ và kiểm tra error log. Thông báo lỗi “Không truy cập được đến máy chủ cơ sở dữ liệu X” liên tiếp liên tiếp được ghi ra log. “Máy chủ X lại có vấn đề gì rồi đây …”. Hắn vừa nghĩ, mắt vừa lướt qua các đồ thị giám sát tài nguyên của toàn bộ hệ thống. “Lưu lượng truy cập vào máy chủ web vẫn bình thường. Tỉ lệ cachehit vẫn không đổi. Mọi thứ không có gì có vẻ bất thường. Vậy vấn đề này ở máy chủ X rồi”. Hắn nghĩ, rồi gõ
ssh X
X là một máy chủ cơ sở dữ liệu chạy mysql, 4 cores 24GB Ram 2 đĩa cứng 300GB RAID 1. Không quá yếu nhưng cũng không quá khoẻ. Vì là máy chủ cơ sở dữ liệu nên phần lớn tài nguyên của X được dùng cho mysql.
“Để xem chú mày bị làm sao nhé!” - hắn bắt đầu công đoạn chẩn đoán bệnh của máy chủ.
Sau khi vào máy chủ X, hắn gõ top. Lệnh top hiện ra máy chủ có 4 cores, tất cả đều có %cpu xấp xỉ 95%. Hắn gõ iostat 1, và quan sát I/O của đĩa cứng. TPS (Trasfer per second) biến động từ 131.89 xuống đến 19.00. tps trung bình không cao. Blk_wrtn/s và Blk_read/s cũng biến động nhưng trung bình cũng không cao.
“CPU hoạt động cật lực trong khi đấy I/O thì không quá lớn”, hắn ghi lại điểm quan trọng này trong đầu. Ghi nhớ xong, hắn tiếp tục mở slow query log ra xem. Log này ghi lại những query mà mysql chạy quá lâu hơn 1s. 1 loạt query kiểu
select * from table_name where video_id in (12345, ‘23434’) and language = ‘en-us’;
được ghi ra log.
Query trên có 2 điểm rất kỳ lạ.
$ mysql -u root -p
Enter password: ***********
mysql> use database database_name;
mysql> show table status like ‘table_name’\G;
*************************** 1. row ***************************
Name: table_name
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 72148
Avg_row_length: 924
Data_length: 66732032
Max_data_length: 0
Index_length: 14630912
Data_free: 7340032
Auto_increment: NULL
Create_time: 2013-10-11 18:33:07
Update_time: NULL
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options:
Comment: Latest translation for vid
Để xem chú mày đang bận rộn xử lý cái gì nhé.
mysql> show process list;
1 loạt query kiểu
“select * from table_name where video_id in (12345, ‘23434’) and language = ‘en-us’;”
“beep, you’ve got mail”. Một mail mới lại về. Máy chủ web đã không thể nào truy cập được X. Zabbix thông báo bản thân zabbix cũng không thể nào truy cập máy chủ X để lấy thông tin giám sát.
“Tình huống có vẻ nghiêm trọng lên.” hắn lẩm bẩm.
Máy chủ bận rộn CPU, I/O không lớn chứng tỏ là query trên tốn rất nhiều CPU. Có lẽ CPU đang tốn thời gian để sắp xếp và tìm kiếm, một mình chứng của việc mysql đang phải tìm với 1 lượng dữ liệu lớn. 70000 không phải con số to, do vậy chỉ có thể là máy chủ X đang phải tìm kiếm mà không có chỉ mục (index)!
“Không lẽ nào!”, vừa nói hắn vừa gõ lệnh
mysql> show index from table_name;
+-------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+-------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| table_name | 0 | PRIMARY | 1 | id | A | 73908 | NULL | NULL | | BTREE | |
| table_name | 0 | PRIMARY | 2 | language | A | 73908 | NULL | NULL | | BTREE | |
| table_name | 1 | idx_table_name_1 | 1 | user_id | A | 24636 | NULL | NULL | | BTREE | |
+-------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
Rất buồn, vậy là video_id có gắn index đàng hoàng. Vậy thì không có lý do gì mà query trên lại không query theo index cả. Thật kỳ lạ. Vậy để thử xem query trên có dùng index không nhé. Đoạn hắn lấy 1 query bất kỳ và thử EXPLAIN.
mysql> explain SELECT * FROM table_name WHERE `video_id` IN (1412240325) AND `language` = “en-us”\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: table_name
type: ALL
possible_keys: PRIMARY,idx_table_name_2
key: NULL
key_len: NULL
ref: NULL
rows: 66870
Extra: Using where
1 row in set (0.00 sec)
ERROR:
No query specified
key: NULL nghĩa là query trên không sử dụng index! Tại sao bảng có chỉ mục mà query lại không dùng index. Chắc chắn là video_id có vấn đề rồi. Vừa nghĩ hắn vừa gõ câu lệnh show create table để xem kiểu dữ liệu lúc tạo bảng.
mysql> show create table table_name;
table_name | CREATE TABLE `table_name` (
`video_id` varchar(34) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`language` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`user_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`video_id`,`language`),
KEY `idx_videotranslationinfo_1` (`user_id`),
KEY `idx_videotranslationinfo_2` (`video_id`),
KEY `idx_videotranslationinfo_3` (`language`),
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Latest translation for videos.' |
Có gì đó không ổn. Query thì coi video_id như là kiểu số nguyên, trong khi bảng lại định nghĩa video_id kiểu xâu dữ liệu. Có lẽ việc khác nhau trong kiểu dữ liệu này làm mysql không so sánh được truy cập với index, làm cho mysql sẽ tìm bản ghi bằng cách lặp toàn bộ bảng. Suy nghĩ vậy, hắn liền thử explain 1 query sau khi đã thay số bằng chữ.
mysql> explain SELECT * FROM table_name WHERE `video_id` IN (“1412240325”) AND `language` = “en-us”\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: table_name
type: ALL
possible_keys: PRIMARY,idx_table_name_2
key: PRIMARY
key_len: NULL
ref: NULL
rows: 66870
Extra: Using where
1 row in set (0.00 sec)
ERROR:
No query specified
“Ồ la la” hắn khẽ reo lên.
Sau khi đổi video_id thành kiểu chuỗi thì index đã được sử dụng key: PRIMARY. Hắn ngay lập tức liên lạc với bên phát triển và để sửa đoạn code sinh ra query trên. Bên phát triển lập tức tìm ra có 1 dòng code chưa gọi strval để biến video_id thành xâu dữ liệu trước ném query cho DB. Bên phát triển lập tức sửa source code và cập nhật phiên bản mới nhất lên máy chủ. Ngay lập tức %cpu của X trở về 1%. Trang web lại vào bình thường như chưa từng có gì cản trở. Slow log query cũng dừng log query hẳn.
Index thật quan trọng và Kiểu dữ liệu cũng rất quan trọng.
Hắn khoái trí khi phát hiện ra hiểu ra được thêm 1 nguyên lý hoạt động của mysql cũng như ảnh hưởng của máy chủ X lên toàn bộ hệ thống. Đôi khi chỉ 1 mặt xích sai sót trong cả 1 dây chuyền có thể phá huỷ toàn bộ dây chuyền - hắn lờ mờ suy nghĩ và ngủ gục. Giờ là 4h sáng.
]]>Là một lập trình viên, tôi có thể khẳng định một điều là tôi ghét quảng cáo! Và tôi chắc chắn 90% các bạn cũng ghét quảng cáo như tôi. Bằng chứng là bạn đang cài adblock extension cho chrome hoặc firefox, hay là chúng ta hay đem hình tượng ‘kangaroo’ (một quảng cáo đã trở thành hiện tượng khi phát vào chung kết C1 năm 2011) là một hình tượng cho sự ‘xấu xa’, ‘phiền phức’ của quảng cáo.
Chúng ta cũng hay dùng quảng cáo như là thước đo cho chất lượng của một kênh truyền hình hay là trang web, ví dụ như: ‘vtv3 dạo này toàn quảng cáo’, hay là ‘trang web abcxyz toàn quảng cáo, lừa đảo đó!’.
Vậy lý do làm sao chúng ta lại ghét quảng cáo đến vậy? Có lẽ nguyên nhân lớn nhất là chúng cản trở chúng ta đến với dịch vụ (che tầm nhìn của trang web hay ti vi), và chúng hiển thị những thông tin mà chúng ta coi là dư thừa, không cần thiết. Tuy nhiên có phải vì thế mà quảng cáo chỉ toàn điều xấu và không đáng tồn tại?
Tuy nhiên, hãy nhìn vào mặt tốt của quảng cáo một chút
Vậy nếu nhìn theo những hướng tích cực này thì quảng cáo không hề xấu, chỉ có cách thức tiến hành quảng cáo tồi đã gây nên những hình ảnh thiếu tích cực với quảng cáo. Vậy với tư cách là người tiêu dùng, chúng ta cần một hệ thống quảng cáo thông minh hơn, mà không cản trở, cũng như cung cấp những thông tin quảng cáo có ích, phù hợp hơn đúng không?
Thực tế thì hệ thống quảng cáo đã và đang thay đổi hàng ngay để thông minh hơn, đến đúng người dùng hơn. Trong bài viết này tôi sẽ giới thiệu qua về hệ thống quảng cáo trên internet nói chung, về khái niệm cũng như cách thức vận hành của chúng. Qua đó bạn sẽ hiểu tại sao google lại chỉ có thể sống được mà chỉ nhờ có quảng cáo, bạn cũng hiểu được tại sao facebook, twitter sẵn sàng cung cấp dịch vụ miễn phí cho bạn. Để bắt đầu, trước tiên chúng ta sẽ đến với các thuật ngữ chuyên môn được sử dụng trong hệ thống quảng cáo.
Do hệ thống quảng cáo ở Việt Nam còn khá non nớt và thô sơ, nên các thuật ngữ trong ngành ít được phổ biến rộng rãi bằng tiếng Việt, do đó ở dưới đây tôi sẽ nói về các thuật ngữ bằng tiếng Anh.
Như vậy chúng ta đã có các khái niệm hết sức cơ bản về các thuật ngữ sử dụng trong quảng cáo, để có cái nhìn đầu tiên cho các bài viết tiếp theo. Trong phần tiếp chúng ta sẽ tiếp tục nói về:
OpenSSL là một bộ thư viện/tiện ích dùng trong mã hoá (cryptography) viết bằng C, open source, và được sử dụng rất rộng rãi trên rất nhiều các phần mềm. OpenSSL cung cấp hầu hết các thuật toán mã hoá nổi tiếng như AES, RSA cũng như các thuật toán hash quan trọng như MD5, SHA1.
Như cái tên của nó, OpenSSL được sinh ra chủ yếu để hỗ trợ cho việc truyên tin qua internet một cách bảo mật thông qua SSL (Secure Socket Layer) và TLS (Transport Layer Security), mà ví dụ rõ ràng nhất là việc sử dụng trên các browser hay là các web server để dành cho các kết nối https.
Tuy nhiên OpenSSL vẫn được sử dụng rộng rãi trong nhiều hoàn cảnh khác nhau, ví dụ như khi bạn chỉ cần tính giá trị SHA1 hash, hay là muốn sử dụng một số thuật toán mã hoá đối xứng như là AES hay DES cho các ứng dụng yêu cầu về tốc độ và thực hiện đơn giản.
Trong thực tế OpenSSL được sử dụng rất nhiều, ví dụ như trong git, để tính giá trị HMAC khi nhận message thông qua imap, git sẽ sử dụng openssl trong trường hợp máy client có cài đặt sẵn bộ thư viện openssl:
https://github.com/git/git/blob/97b8860c071898d9e162678ea1035a8ced2f8b1f/imap-send.c#L861
Như vậy chúng ta có thể hình dung openssl là bộ thư viện (có thể gọi là qui chuẩn) dành để làm các công việc liên quan đến mã hoá.
OpenSSL là một bộ thư viện viết bằng C, còn android bản chất là hệ điều hành linux. Do đó việc cài đặt OpenSSL trên Android các bạn có thể hình dung tương tự như cài đặt một thư viện trên linux, cũng có make, có build, có copy file thư viện vào các đường dẫn cần thiết.
OpenSSL là một thư viện đồ sộ và khá phức tạp để build. Tuy nhiên rất may mắn là những người phát triển OpenSSL đã bỏ thời gian ra làm cho chúng ta một bản hướng dẫn cực kì đầy đủ để build từ source code và sử dụng trên android. Các bạn có thể tham khảo ở đường dẫn dưới đây:
http://wiki.openssl.org/index.php/Android
Làm theo hướng dẫn trên sẽ giúp các bạn tạo ra được 2 file (libcrypto.so libssl.so) hoặc (libcrypto.a libssl.a) tuỳ theo setting lúc build. File .so và file .a là các file thư viện động và tĩnh, mà các hàm trong các thư viện đó có thể được gọi trực tiếp từ C code. Cả .so và .a file đều có thể được gọi dễ dàng chỉ bằng việc thay đổi ndk make file. Do bản chất của ndk như đã trình bày ở phần 1, từ android OS muốn gọi được logic từ C code phải thông qua JNI interface, chúng ta có thể hình dung được qui trình để sử dụng openssl trên ndk theo từng bước như sau:
Ở dưới đây chúng ta sẽ lần lượt đi từng bước ở trên. Đầu tiên sẽ là việc quan trọng nhất là sử dụng openssl trên C ra sao.
OpenSSL thường được sử dụng dưới dạng “utility” trên unix system, tức là bạn sẽ gọi thông qua command line, ví dụ như sau:
1
|
|
Dòng lệnh trên ở trên console sẽ được sử dụng để tính hash của nội dung file digest.txt theo thuật toán SHA1, và ghi nội dung của hash vào file file.txt.
Tuy nhiên bài toán của chúng ta ở đây là cần sử dụng openssl trong “code” chứ không phải thông qua command line. Việc sử dụng openssl trong code phức tạp hơn khá nhiều so với command line. Lý do là các thuật toán mã hoá đều khá phức tạp, và để sử dụng trong code thì đòi hỏi hiểu biết về thuật toán mã hoá đang sử dụng sâu hơn. Trong bài toán như tôi đã trình bày trong phần 1, chúng ta sẽ implement một thuật toán mã hoá đối xứng thông qua openssl. Do đó trước khi bắt tay vào coding, chúng ta hãy tìm hiểu sơ qua về thuật toán mã hoá đối xứng.
Ở phần 1 đã nói sơ qua về thế nào là mã hoá đối xứng. Một cách đơn giản, thuật toán mã hoá đối xứng là khi bên gửi và bên nhân sẽ dùng cùng một key, cùng một initialize vector
Thuật toán mã hoá đối xứng chia làm 2 loại chính: block cipher và stream cipher.
Stream cipher thì sẽ có tốc độ nhanh hơn rất nhiều so với Block cipher, tuy nhiên vì chỉ đơn giản thực hiện phép XOR sẽ làm Stream cipher có một số thuộc tính làm nó trở nên kém an toàn hơn so với Block cipher. Do đó trong bài toán lần này chúng ta sẽ sử dụng Block cipher.
Block cipher có khá nhiều “mode”. Mỗi “mode” có thể hiểu là các cách thức tiến hành mã hoá khác nhau. Cơ bản thì sẽ có 4 loại mode dưới đây:
Ở bài toán của chúng ta, có thể thấy rằng CBC mode là lựa chọn tốt nhất. Việc tiếp theo là lựa chọn thuật toán mã hoá.
Có thể kể ra một vài thuật toán mã hoá đối xứng, sử dụng BlockCipher tiêu biểu gồm có : AES, BlowFish, DES, TripleDES. Trong đó AES (Advanced Encryption Standard) là thuật toán được tạo ra gần đây, có thể sử dụng key và độ dài block lên tới 256 bit. AES được chính phủ Mĩ sử dụng làm tiêu chuẩn mã hoá, và là một thuật toán mã hoá đã được nghiên cứu rất kỹ lưỡng trong vòng 5 năm. Do vậy mà so với các thuật toán còn lại như Blowfish hay DES, AES đảm bảo được độ an toàn cao hơn. Trong lần này chúng ta sẽ sử dụng AES 256 bit, trên CBC mode.
Như chúng ta đã thấy ở trên, mỗi loại thuật toán mã hoá, mỗi mode đều có những con đường (routines) khác nhau để thực hiện. Do đó nếu mỗi con đường đó được thực hiện với những interface khác nhau sẽ rất khó nhớ và khó để thực hiện. Rât may mắn, OpenSSL cung cấp sẵn cho chúng ta một interface thống nhất cho một loạt các thuật toán mã hoá khác nhau, gọi là EVP. Thông qua EVP thì qui trình mã hoá trở nên rất đơn giản thông qua việc gọi lần lượt các hàm của EVP. Để tiến hành mã hoá
1 2 3 4 |
|
Để tiến hành giải mã chúng ta cũng dùng các hàm gần tương tự, chỉ thay Encrypt bằng Decrypt
1 2 3 4 |
|
Sử dụng những kiến thức đã được nói ở phần trên, chúng ta đã có thể tiến hành coding. Một đoạn sample code sử dụng openssl để mã hoá đối xứng theo AES 256bit được mô tả như dưới đây. Chúng ta sẽ đặt tên file dưới đây là security.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
|
Ngoài việc sử dụng các kiến thức đã nói ở trên, chúng ta có thể chú ý thấy một số điểm đặc biệt ở đoạn code trên:
Như vậy là chúng ta đã tiến hành xong công đoạn coding. Công đoạn tiếp theo không kém phần quan trọng là việc phải build được đoạn code đó thành thư viện native để sử dụng trên android OS. Để làm được việc đó chúng ta cần làm:
Cấu trúc folder theo như bài viết lần đầu, chúng ta sẽ tạo 1 folder jni ở project$ROOT. Trong đó sẽ được sắp xếp như sau
Chúng ta có thể thấy điểm đặc biêt ở đây là thư mục libprebuilt sẽ chứa các file .so của openssl được build cho từng platform khác nhau. Hiện tại android có thể chạy trên ARM(armeabi), Intel(x86) và MIPS. Do việc build ra thư viện .so từng platform khác nhau có thể gặp khá nhiều khó khăn nên chúng ta có thể làm theo 1 cách đơn giản hơn, đó là kiếm các file .so “có sẵn” của từng platform và copy vào đây, thay vì phải build tử source. Các file này có thể kiếm được dễ dàng từ bản phân phối của các image của android OS.
Một điểm nữa cần lưu ý là chúng ta cần copy các file header cần sử dụng của openssl vào trong thư mục dự án thì mới include được.
2 file make để build native source sẽ có nội dung như sau
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1
|
|
Chúng ta có thể chú ý thấy điểm đặc biệt ở Android.mk. Trong make file này chúng ta sẽ thấy việc chỉ định các biến build PREBUILT_SHARED_LIBRARY, LOCAL_SRC_FILE, LOCAL_MODULE để hệ thống build của ndk có thể nhận đưọc sự tồn tại của các file .so và copy vào các folder cần thiết để gọi được sau trên java code.
Để tiến hành build thì chúng ta chỉ cần vào thư mục dự án và gõ
1
|
|
Sau khi tiến hành build thì trong thư mục /libs sẽ có các thư mục tương ứng với các platform được tạo ra, và các file .so cần thiết sẽ được copy vào trong đó. File security.c ở trên sẽ được build thành các file security.so tương ứng.
Tiếp theo chỉ còn là vấn đè sử dụng các file .so trên java code:
1 2 3 4 5 6 7 8 9 |
|
Chỉ với các chỉ định như trên thì chúng ta đã có thể sử dụng được hàm stringFromJNI được code trong security.c. Khi truyền vào 1 chuỗi bất kỳ, thì chúng ta sẽ nhận được kết quả mã hoá của chuỗi đó theo AES 256bit, với key và iv được qui định trong security.c. Vậy là bài toán của chúng ta đã được giải quyết :D.
Qua hai bài viết tương đối đầy đủ, hy vong các bạn đã nắm được:
Tham khảo:
]]>Đó là lý do tôi chuyển sang lập trình OpenCV với Python. Việc cài đặt, viết code, thử nghiệm đều trở nên đơn giản hơn rất nhiều.
Thêm một lý do nữa, phải nói là Computer Vision và Image Processing là những lĩnh vực khó nếu xét về khía cạnh nghiên cứu, với rất nhiều công thức toán. Tôi không nói những công thức toán đó là không cần thiết, mà là đối với người bắt đầu học, sẽ tốt hơn nếu nhanh chóng nắm được tất cả những kĩ thuật cơ bản và bắt đầu áp dụng xây dựng ứng dụng hữu ích cho mình. Sau đó, tuỳ theo nhu cầu và sở thích, có thể tiếp tục đào sâu nghiên cứu, tối ưu, chạy thời gian thực… Kể cả lúc đó, khi đã có kiến thức cơ bản rồi, việc tiếp cận sẽ càng nhanh hơn.
Xin lỗi vì phần giới thiệu hơi dài dòng :P Tôi xin bắt đầu luôn. Trong bài đầu tiên này, tôi sẽ trình bày cách cài đặt môi trường và một số tác vụ cơ bản khi làm việc với OpenCV trên Python. Bài viết này có thể sẽ khá cơ bản với một số độc giả, nhưng vì là bài đầu tiên trong loạt bài này, tôi vấn muốn trình bày để đảm bảo sự đầy đủ. Trong các bài sau, chúng ta sẽ cùng xây dựng những ứng dụng thú vị hơn.
Có những thành phần sau cần phải cài đặt để làm việc với OpenCV trên : * Python 2.7 * NumPy và SciPy: Bằng cách sử dụng NumPy, chúng ta có thể biểu thị hình ảnh bằng mảng đa chiều. Ngoài ra, có rất nhiều thư viện xử lý ảnh và cả machine learning sử dụng cách biểu thị mảng của NumPy. NumPy cũng hỗ trợ rất nhiều hàm toán học chúng ta có thể thực hiện nhiều phân tích có ý nghĩa hơn trên ảnh. Bên cạnh NumPy còn có SciPy đi kèm, hỗ trợ thêm về các tính toán khoa học.
Trên Windows, cách dễ nhất để có cả Python, NumPy và SciPy là cài đặt Enthought Canopy tại https://www.enthought.com/products/canopy/
Trên Mac từ 10.7 trở lên, NumPy và SciPy đã được cài đặt sẵn. * Matplotlib: là thư viện để plot. Khi xử lý ảnh, chúng ta sẽ sử dụng thư viện này để xem histogram của ảnh hoặc xem chính ảnh đó. * OpenCV: Về hướng dẫn cài đặt chi tiết, bạn có thể xem tại đây
Hãy bắt đầu bằng tác vụ đơn giản nhất: mở một file ảnh thành một mảng NumPy, đọc các thông tin về kích cỡ, sau đó lưu mảng NumPy thành một file ảnh mới.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Đầu tiên, chúng ta import hai package cần thiết là argparse
và cv2
. argparse
dùng để khai báo những parameter cần thiết cho chương trình của chúng ta, ở đây, chúng ta khai báo parameter là đường dẫn đến file ảnh ban đầu. Để đọc file ảnh thành một mảng NumPy, chúng ta sử dụng hàm cv2.imread()
với parameter là tên file ảnh (đã được nhập vào từ command line). Sau bước này, ta nhận được một mảng NumPy là image
. Tiếp theo ta in ra các thông số về file ảnh, gồm có chiều dài, chiều rộng và số channel. Ở đây, chúng ta có những điểm lưu ý về cách NumPy lưu các thông số về bức ảnh: * Một bưc ảnh có 2 chiều X và Y, gốc toạ độ tại pixel trên cùng bên trái của bức ảnh. Chiều X từ trái sang phải và chiều Y từ trên xuống dưới. NumPy lưu số pixel hơi ngược: image.shape[0]
là số pixel theo chiều Y và image.shape[1]
là số pixel theo chiều X. * Mỗi pixel trên bức ảnh được biểu thị dưới một trong 2 dạng: grayscale hoặc color. image.shape[2]
lưu số channel biểu thị mỗi pixel. Với ảnh màu hiển thị trên RGB, số channel là 3, còn với ảnh đen trắng (grayscale), chúng ta chỉ có 1 channel duy nhất
Để hiển thị bức ảnh trên một window mới, ta sử dụng hàm cv2.imshow()
, với 2 đối số là tên của window và tên của mảng NumPy muốn hiển thị.
Để lưu mảng NumPy thành một file ảnh mới, ta sử dụng hàm cv2.imwrite()
, với 2 đối số là tên của file muốn lưu và tên của mảng NumPy. Trong ví dụ này, chúng ta lưu file mới giống hệt file cũ mà không có chỉnh sửa gì.
1
|
|
Ảnh gốc:
Kết quả trả về
1 2 3 |
|
Trong phần này, chúng ta sẽ tạo ra ảnh avatar từ một bức hình chân dung ban đầu, theo dạng như avatar picture của Google+: hình tròn bao quanh khuôn mặt. Đoạn code như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Đoạn code bắt đầu với các thủ tục như import packages cần thiết, khai báo các parameters đầu vào cho script, đọc file ảnh vào mảng NumPy như phần trên.
Để crop một phần bức ảnh, chúng ta cần tạo ra một mặt nạ mask
, là một mảng có kích thước như bức ảnh với tất cả giá trị pixel được khởi tạo bằng 0. Tiếp đó, ta vẽ một hình tròn trắng trên mảng ‘mask’ . Sau đó, sử dụng bitwise_and()
hai mảng image
với nhau, có thêm tham số mặt nạ mask
, ta được một bức ảnh masked
chỉ có phần khuôn mặt. Tiếp tục crop bằng cách slice mảng NumPy, chúng ta được kết quả cuối cùng là bức ảnh avatar
.
1
|
|
Kết quả:
mask
avatar
Tất nhiên, tôi đã sử dụng file ảnh có mặt tổng thống Obama ở ngay giữa tấm ảnh và mask được đặt ở chính giữa tâm bức ảnh. Không phải mọi tấm ảnh đều như vậy; trong trường hợp đó chúng ta sẽ sử dụng kĩ thuật face detection. Nhưng đó là chủ đề của một bài viết sau.
Qua bài viết này, tôi đã giới thiệu những bước bắt đầu để làm việc với OpenCV qua Python. Bài viết khá cơ bản, nhưng đã trình bày một số khái niệm cơ bản để chuẩn bị cho những ứng dụng lần sau. Hẹn gặp lại các bạn trong các phần tiếp theo.
https://ktmt.typeform.com/to/WzOLiB
Những bạn nào vẫn chưa làm mà có hứng thú làm thì nhóm bọn mình vẫn hết sức welcome.
Bản điều tra có mục đích chính là để tìm hiểu thực trạng và thói quen của các lập trình viên Việt Nam, qua đó điều chỉnh các bài viết của blog sao cho có ích với càng nhiều người càng tốt.Ở bài viết này mình sẽ tổng kết về một số con số mà mình cảm thấy thú vị, hy vọng có ích với mọi người.
Đầu tiên là tính đến thời điểm viết bài này, bản điều tra nhận được 256 câu trả lời trên tổng số 416 lượt visits (tỉ lệ là cứ 5 người xem thì 3 người trả lời). Thời gian trả lời trung bình mất tầm 4 phút, và tỉ lệ hoàn thành 100% bản điều tra là 62% (Tuy nhiên phần lớn các câu hỏi đều có tỉ lệ trả lời tiệm cận 99%, thế nên con số 62% này không có giá trị lắm)
Sau đây là một số kết quả thú vị, mình sẽ đi lần lượt các câu hỏi từ trên xuống:
10/11. 50% số bạn trả lời sau khi tìm được câu trả lời cho điều mình tìm kiếm thì không lưu lại, có lẽ mọi người cho rằng tìm được kết quả là được, không quan tâm lắm đến việc lần sau gặp lại lỗi đấy thì sẽ phải mất công tìm lại lần nữa.
Trên đây là một số con số mình thấy khá thú vị và đã tổng kết lại. Rất cám ơn mọi người đã làm bản điều tra giúp bọn mình! Hy vọng mọi người tiếp tục ủng hộ blog trong thời gian tới!!!!
]]>Gần đây ở công ty tôi có được giao một task khá hay. Công ty tôi có một game viết trên nền tảng android. Game đó viết bằng anđroid, tuy nhiên lại chủ yếu dùng web view để hiển thị. Mặc dù vậy, một số logic như là set session cho user, authenticate cho user thì lại nằm trên android.
Chắc các bạn cũng đã biết, android app được viết bằng java, dịch ra file dex, sau đó được phân phối trên google playstore dưới dạng file apk. Do đó, android app có một điểm yếu cố hữu mà mọi java app đều mắc phải, đó là bảo mật. Điểm yếu bảo mật ở đây là gì? Đó là việc mà mọi java app đều có thể được phân tích ngược (reverse engineer) rất dễ dàng. Việc này bắt nguồn từ bản chất java được dịch ra bytecode ở dạng khá “gần” với ngôn ngữ lập trình thông thường, và bytecode chứa đầy đủ các thông tin cần thiết để bạn có thể dịch lại nguyên vẹn lại chương trình gốc.
Vậy cái điểm yếu bảo mật này liên quan đến cái app tôi đang phụ trách thế nào? Như tôi vừa nói ở trên, trong cái game mà tôi đang phụ trách, logic authenticate cho user sẽ nằm trên phía android. Điều này có nghĩa là trên android app sẽ phụ trách:
Chắc hẳn sẽ có bạn thắc mắc là qui trình xác thực này quá đơn giản. Đúng vậy, quy trình này quá đơn giản, dẫn đến là việc chỉ cần user A (người xấu) biết uuid của user B (người bị hại) thì A sẽ giả mạo được bất cứ hành động của B như là gửi đồ từ B cho A.
Vậy tại sao không làm một qui trình xác thực tốt hơn, như dùng thêm một token giống như onetime password mà chỉ user đó mới biết được, hay là làm cách nào để “giấu” uuid đi để cho user khác không biết. Đúng là nên như thế! Tuy nhiên vì một số lý do “lịch sử” của legacy code, mà chúng ta không thể thay đổi qui trình xác thực một cách dễ dàng như thế được.
Như vậy thì với flow code hiện tại thì với điểm yếu của android tôi đã nói ở trên thì một người có chút kiến thức lập trình có thể dễ dàng dịch ngược đoạn logic dùng để xác thực mà tôi đã nói ở trên. Mà trong đó có việc mã hoá uuid người dùng
mà khi bạn nhìn được logic code thì mã hoá cũng bằng thừa. Lý do tại sao lại bằng thừa vì code hiện tại đang sử dụng “Symmetric Cryptography Algorithm”. Symmetric ở đây có nghĩa là thuật toán mã hoá đối xứng, mà điển hình gồm có những thuật toán như blowfish, AES, DES.
Nói một cách đơn giản thì các loại thuật toán symmetric thì bên gửi và bên nhận sẽ dùng cùng một key, cùng một intitialize vector (Các khái niệm này tôi sẽ trình bày kĩ hơn ở phần sau) , do đó chỉ cần dịch ngược được code thì user A (người xấu) sẽ có được key và initialize vector để tạo ra một request hợp lệ sử dụng uuid của user B.
Vậy thì chúng ta phải giải quyết vấn đề này thế nào? Sau một hồi thảo luận với công ty thì tôi nghĩ ra một giải pháp “chữa cháy” tạm thời, đấy là chuyển logic vào native code sử dụng ndk và C, mục đích để đạt được là:
“Giấu” đi logic mã hoá uuid người dùng, giấu cả các tham số ban đầu như key và initialize vector. Do đó mà user A sẽ không biết làm cách nào để tạo ra một request hợp lệ với uuid của user B.
Cách giải quyết này tại sao tôi nói là tạm thời, bởi vì user A nếu có thêm một chút hiểu biết về ndk thì sẽ biết được interface cung cấp ở ndk code sẽ được public ra ngoài, do đó thì vẫn có thể tận dụng được điểm này để tạo ra một request hợp lệ. Tuy nhiên do không nghĩ ra giải pháp khác nên tạm thời dùng cách này sẽ hạn chế được các hacker “gà mờ”.
Vậy để đi theo hướng đi này chúng ta cần phải tìm hiểu về 2 thứ đó là : Android NDK và cách để sử dụng các thuật toán mã hoá trên ndk (ở đây là sử dụng ngôn ngữ C), đó là openssl. Phần giới thiệu hơi dài dòng, nhưng đến đây các bạn đã nắm được tại sao tiêu đề bài viết lại là Android NDK và open SSL.
Dưới đây chúng ta sẽ đi lần lượt về 2 vấn đề cần giải quyết : Android NDK và OpenSSL
Android NDK là một kit phát triển giúp bạn có thể phát triển các phần mềm android mà dựa một phần trên các đoạn code viết trên C hoặc C++. Bạn sẽ cần đến NDK trong các sản phầm cần đến hiệu năng cao, mà khi đó các đoạn code được build ra binary sẽ phát huy hiệu năng tối đa. Các logic code được thực hiện trên ndk ở dứoi đây tôi sẽ gọi chung là native code.
Về cơ chế hoạt động của ndk, bạn có thể hiểu một cách đơn giản như trong hình vẽ dưới đây, app của bạn sẽ tiến hành giao tiếp với native code thông qua một interface gọi là JNI.
Một cách đơn giản, JNI là một bộ giao thức giao tiếp chuẩn của java, giúp cho java code có thể nói chuyện được với C/C++ code, có thể truyền dữ liệu giữa 2 bên.
Để tham khảo thêm về android ndk, các bạn có thể vào trang chủ của android tại Trang chủ của android. Dưới đây tôi sẽ tóm tắt các bước cần thiết để sử dụng được ndk.
Cách cài đặt android ndk khá giống với sdk, tức là chỉ đơn thuần là bạn tải bộ ndk về, đặt vào đâu đó. Trong bộ NDK đó sẽ chứa đầy đủ các tool để có thể build được ndk native code từ C/C++ source (bao gồm build script và các file header cần thiết). Quá trình cài đặt có thể hiểu tóm gọn qua đoạn script dưới đây (chạy trên môi trường unix):
1 2 3 4 5 6 |
|
Sau khi chạy đoạn script trên thì android ndk đã được thêm vào path của hệ thống, giúp chúng ta có thể gõ các lệnh như ndk-build
từ bất kì đâu
Trong bộ ndk bạn down về có chứa sẵn khá nhiều ví dụ về cách sử dụng ndk, từ đơn giản (như hello world) cho đến các ví dụ phức tạp hơn như xử lý ảnh (mà phải thao tác gửi dữ liệu giữa android app và ndk app khá phức tạp). Các bạn có thể tham khảo các ví dụ đó để có cái nhìn thực tế về ndk program. Dưới đây tôi sẽ trình bày ngắn gọn về quá trình sử dụng của tôi.
Như ở hình ở trên thì các bạn thấy là android app và native code sẽ “nói chuyện” với nhau thông qua một “ngôn ngữ” chung gọi là jni. Như vậy sẽ có 2 khả năng xảy ra, dẫn đến 2 ngữ cảnh để sử dụng ndk:
Ở bài viết này tôi sẽ đi theo hướng tiếp cận 1, để giải quyết bài toán theo hướng:
Đưa logic mã hoá uuid người dùng vào một file C, build ra binary và gọi logic đó trên phía java thông qua JNI.
Để đi theo hướng tiếp cận 1 như đã nói ở trên, chúng ta có thể dễ dàng hình dung công việc phải làm:
Thông thường, chúng ta sẽ tạo một folder tên là jni và đặt toàn bộ các đoạn code, header, các thư viện liên quan vào trong đó.
File native code viết trên C khá đơn giản, chỉ cần tóm gọn lại trong 2 bước:
Một ví dụ hết sức về native code như dưới đây:
1 2 3 4 5 6 7 8 9 |
|
Các bạn để ý tên hàm của native code sẽ dễ dàng nhận thấy convention như trong hình dưới đây:
Nhờ có convention đó mà các bạn sẽ thấy việc gọi logic của hàm đó trên phía java sẽ dễ dàng hơn bao giờ hết.
Ngoài ra các bạn có thể để ý một số điểm đặc biệt ở một đoạn native code như dưới đấy:
Để build được file native C mà chúng ta vừa viết ở trên, chúng ta cần làm 2 việc:
ndk-build
ở trong folder hiện tại.Sau khi sử dụng lệnh ndk-build để build thì kết quả build là các file .so sẽ được copy vào thư mục libs ở root folder theo như hình trên đây. Các bạn có thể thấy là tương ứng với mỗi kiến trúc CPU sẽ có một folder được tạo ra, trong mỗi folder đó lại có các file .so khác nhau chỉ dùng với duy nhất một kiến trúc nhất định.
Đã build xong thư viện tĩnh, chúng ta chỉ còn một công đoạn cuối cùng là sử dụng đoạn logic ở trên trong android code. Theo như ở trên đã nói, interface của jni code sẽ được sử dụng dựa theo convention mà gồm có: package name, class name và cfunction name. Điều đó có nghĩa là: đoạn code java trong android của bạn sẽ phải có package name, class name và function name y hệt như interface của jni, thì bạn mới sử dụng được logic đó.
Vậy thì theo như ví dụ của chúng ta ở đây thì chúng ta cần phải làm 3 việc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Từ đoạn code trên chắc các bạn đã hình dung ra cách để gọi native code thế nào dựa vào hàm System.loadLibrary("hello-jni")
và việc định nghĩa hàm thông qua directive native
Như vậy chúng ta đã tìm hiểu rất sơ qua về ndk. Trong phần tiếp theo, tôi sẽ đi vào phần chính mà tôi muốn nói đến, đó là giới thiệu về openssl và sử dụng openssl trên android ndk.
]]>Nếu bạn là một hacker làm việc nhiều với Mac hoặc Linux, chắc các bạn chẳng xa lạ gì với terminal - giao diện dòng lệnh cơ bản nhất của hệ điều hành Unix. Tôi là một hacker bị “cuồng terminal”, zsh
, prezto
, tmux
, irssi
, vim
, tig
là những tools ưa thích nhất. Tôi từng có ước mơ muốn từ bỏ các giao diện đồ hoạ, có thể lập trình, chat chit, nghe nhạc v.v.. ngay trên môi trường không-đồ-hoạ.
Bên cạnh đó, mặc dù không mấy mặn mà với Facebook nhưng gần đây lại bị nghiện Twitter, trong đầu tôi luôn hiện lên câu hỏi: làm thế nào để cũng có thể tương tác với Twitter chỉ qua terminal của MacOSX ?
Trên thực tế đã có khá nhiều thư viện mã nguồn mở có thể đáp ứng được nhu cầu trên. t hay earthquake là những gem(Ruby) được viết rất bài bản và đa tính năng. Tuy nhiên tôi đã quyết định tự viết một phần mềm của riêng mình, bởi tự phát triền và làm sản phẩm của mình được cộng đồng đón nhận là một mục tiêu mới mẻ và đầy thử thách.
Trong bài viết này, tôi sẽ giới thiệu với các bạn tôi đã xây dựng một phần mềm mã nguồn mở như thế nào, về cả kỹ năng phát triển và cách mang phần mềm của mình đến với cộng đồng hacker trên thế giới.
Khi bạn bắt đầu viết một phần mềm mã nguồn mở, điều quan trọng đầu tiên sẽ là : đã có ai thực hiện ý tưởng của bạn chưa và họ đã thực hiện được tốt đến đâu. Khi chuẩn bị viết phần mềm của mình, tôi nhận thấy t giống như 1 twitter command trên Unix, focus vào khả năng pipe với các command khác. Ngược lại, earthquake là 1 app hoàn chỉnh nhưng xử lý hiển thị tweets lại chưa thật tốt.
Và từ đó Rainbow Stream ra đời. Bạn có thể nhận ra 2 điểm nêu trên khi nhìn vào cách thức hoạt động của app dưới đây:
Để gây được ấn tượng với người dùng, sản phẩm của bạn vẫn cần có 1 đến 2 tính năng nổi trội. Bạn sẽ không muốn phần mềm mình viết ra mãi chỉ là “alternative to xxx or yyy, can consider if zzz stops development”. Ở đây, tôi xây đựng Rainbow Stream tập trung vào 2 tính năng chính:
Chúng ta sẽ đi vào cụ thể trong các phần tiếp theo.
Hầu hết các terminal hiện đại đều hỗ trợ hiển thị 256 ANSI colors. Trên shell bạn có thể dễ dàng in ra chữ theo các màu định sẵn bẳng các dùng Escape character như dưới đây
1 2 3 4 5 6 7 |
|
Hiện thị màu trên Python có thể được viết gọn theo function như sau
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Sử dụng những function ở trên thực tế rất đơn giản: gọi thẳng function với parameter là mã ANSI color, trả về là một function khác và lần này nhận parameter là string để đổi thành string có màu tương ứng.
1 2 3 4 5 6 7 8 9 10 |
|
Giả sử chúng ta có một tập vô hạn các word không biết trước. muốn mỗi word có một màu và các word lặp lại sẽ có màu giống nhau, chúng ta có thể dùng Memoization trong Python như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Các màu sắc hiển thị trong Rainbow Stream đều dựa theo nguyên lý nói trên.
Để nói cụ thể về phần này sẽ hơi dài dòng, nhưng có thể tóm gọn trong các ý sau đây:
Pillow
. Pillow
cung cấp những tính năng cơ bản để thao tác với lượng thông tin trong một tấm ảnh. Nhược điểm của Pillow là khá buggy khi install và không hỗ trợ Window.Pillow
để đọc thông tin về từng Pixel trong một ảnh, mỗi pixel sẽ có 4 chỉ số gồm 3 chỉ số màu (R,G,B) và 1 chỉ số về độ trong (A).term_color
ở đoạn trên.Trong các bước trên thì bước quy đổi màu là quan trọng nhất. Thuật toán quy đổi dùng ở đây là phương pháp tính khoảng cách vector trong không gian Euclide 3 chiều:
((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)**0.5
Như vậy “ảnh” ở trên terminal thực chất là các ký tự Space với màu ANSI đã được quy đổi và in ra liên tiếp :)
Để hoàn thiện Rainbow Stream thực ra cần một số kỹ năng khác như lập trình với thread, tạo interactive input bẳng readline, gọi chương trình C compile sẵn hay xử lý chung cho cả Python 2 và Python 3… Trong khuôn khổ một bài viết tôi khó có thể trình bày hết những vấn đề trên, vì vậy nếu bạn quan tâm hãy mở thẳng Github repo và đọc source code. Rainbow Stream là một phần mềm mã nguồn mở với MIT license.
(… còn tiếp - Làm thế nào để mang phần mềm của mình đến với thế giới hacker …)
]]>Load Average – tạm dịch là “giá trị tải trung bình” – là một chỉ số liên quan đến CPU rất cơ bản và quan trọng. Việc nắm rõ ý nghĩa của chỉ số này giúp chúng ta đánh giá được hiệu năng hiện thời của máy tính cũng như sử dụng CPU nói riêng, máy tính nói chung một cách hiệu quả nhất
Bài viết này bắt đầu bằng việc giải thích ý nghĩa của “giá trị tải trung bình”. Sau đó bài viết sẽ trình bày cách đánh giá chỉ số này trong thực tế. Cuối cùng bài viết đưa ra một trường hợp thực tế về cách đánh giá hiệu năng máy tính theo chỉ số này.
Để hiểu *tải trung bình" là gì ta sẽ xem xét một ví dụ thực tế như sau.
Bạn đang tham gia giao thông trên đường cao tốc và trước mặt của bạn là trạm thu phí đường bộ. Bạn giảm tốc để chuẩn bị qua cửa soát vé. Trạm xoát vé có 4 cửa soát vé. Tất cả các cửa đều trống và bạn chọn cửa số 1 như dưới đây.
H <-- Xe ôtô của bạn
| 1 | 2 | 3 | 4 | <-- Trạm thu phí
Bạn đánh xe đến cửa số 1, trả phí cho nhân viên soát vé. Nhân viên soát vé mở barrier chắn, và bạn đi qua. Có duy nhất xe bạn qua trạm nên từ phía trạm thu phí, trạm đang phục vụ 1 xe.
| H | 2 | 3 | 4 |
Giờ tưởng tượng có nhiều xe khác cũng lưu thông trên đường cao tốc. Giả sử có trước khi bạn đến trạm thu phí, đang có 2 xe khác làm thủ tục ở cửa số 1 và số 2, bạn chú ý cửa số 3,4 còn trống nên lái xe qua cửa số 3 và làm thủ tục mà không phải chờ đợi. Trạm phục vụ 3 xe.
| H' | H' | H | 4 |
Có thể thấy ở 2 trường hợp trên, trạm thu phí đang làm việc khá hiệu quả. Các xe ôtô đi qua với thời gian chờ đợi bằng 0. Các xe ôtô đi qua trạm xoát vé một cách nhanh chóng. Người lái xe là bạn cảm thấy thoải mái vì không phải chờ đợi.
Giả sử hôm nay là ngày nghỉ lễ, mọi người về quê đông nên xe khách chạy rất đông. Các gia đình tranh thủ ngày lễ nên cũng đánh xe đi chơi xa. Đường cao tốc trở nên đông đúc. Bạn đến trạm thu phí và nhận ra rằng 4 cửa đang có xe làm thủ tục. Chưa kể bạn còn đến sau 2 xe khác và phải đợi xếp hàng sau 2 xe này.
H <-- Xe ôtô của bạn
H'
H'
| H' | H' | H' | H' | <-- Trạm thu phí
Trong trường hợp này, bạn chắc chắn sẽ phải chờ, không những chờ các xe đang làm thủ tục tại cửa trạm mà còn chờ cả các xe đến trước bạn. Thời gian có thể bị kéo dài vì nhiều lý do như 1 xe làm thủ tục mất thời gian hơn các xe khác hoặc có sự cố ở cửa soát vé. Đứng từ góc độ của trạm thu phí, trạm đang phải xử lý số lượng xe (7 xe) nhiều hơn khả năng của trạm (4 cửa). Tại thời điểm hiện tại, trạm đang bị quá tải.
Ta định nghĩa số lượng tải trung bình của trạm là số lượng xe mà trạm phải phục vụ trong một đơn vị thời gian. Như vậy ở ví dụ trên trung bình tải của trạm thu phí tại thời điểm bạn đến là 7.
Tương tự như ví dụ trạm soát vé, “Load Average” của CPU được định nghĩa là số lượng process cần tài nguyên tính toán của CPU tại thời điểm nhất định. Giả sử tải trung bình của máy tính bạn hiện tại là 3.2, điều đó có nghĩa là tại thời điểm đó đang có trung bình 3.2 processes cần CPU xử lý. Tại thời điểm process cần CPU, nếu CPU đang rảnh process sẽ được OS cho chạy trên CPU rảnh.
Mổi “cửa soát vé” trong CPU máy tính sẽ là 1 lõi CPU. Với các CPU thế hệ mới trang bị công nghệ Hyperthreading, mỗi lõi vật lý có thể hoạt động được như 2 lõi logic. Vì vậy OS sẽ nhận diện 8 lõi. Ví dụ máy tính của bạn được trang bị chip mới nhất hiện tại Corei7 MQ– 4 cores 8 threads với công nghệ Hyperthreading thì đối với hệ điều hành máy tính của bạn có 8 cores.
Để kiểm tra máy tính của bạn có bao nhiêu lõi (cores), trên windows bạn có thể kiểm tra qua TaskManager > Performance. Bên cạnh biểu đồ tỉ lệ sử dụng CPU nói chung là tỉ lệ sử dụng CPU của từng lõi. Số lượng cửa sổ bên tay phải là số lượng lõi logic.
Trên Linux bạn có thể kiểm tra bằng nhiều cách:
$ top
# ấn 1
top - 20:38:48 up 2 days, 4:50, 1 user, load average: 11.30, 11.54, 10.17
Tasks: 430 total, 2 running, 428 sleeping, 0 stopped, 0 zombie
Cpu0 : 20.5%us, 2.4%sy, 0.0%ni, 76.2%id, 0.4%wa, 0.0%hi, 0.5%si, 0.0%st
Cpu1 : 20.2%us, 1.9%sy, 0.0%ni, 77.4%id, 0.5%wa, 0.0%hi, 0.1%si, 0.0%st
Cpu2 : 19.9%us, 1.8%sy, 0.0%ni, 77.7%id, 0.5%wa, 0.0%hi, 0.1%si, 0.0%st
Cpu3 : 19.9%us, 2.3%sy, 0.0%ni, 77.2%id, 0.2%wa, 0.0%hi, 0.4%si, 0.0%st
Cpu4 : 19.8%us, 2.3%sy, 0.0%ni, 77.1%id, 0.4%wa, 0.0%hi, 0.4%si, 0.0%st
Cpu5 : 19.7%us, 2.3%sy, 0.0%ni, 77.4%id, 0.2%wa, 0.0%hi, 0.4%si, 0.0%st
Cpu6 : 20.1%us, 1.6%sy, 0.0%ni, 78.1%id, 0.1%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 : 19.6%us, 2.2%sy, 0.0%ni, 77.7%id, 0.1%wa, 0.0%hi, 0.3%si, 0.0%st
Cpu8 : 19.4%us, 2.2%sy, 0.0%ni, 78.0%id, 0.1%wa, 0.0%hi, 0.3%si, 0.0%st
Cpu9 : 19.8%us, 2.2%sy, 0.0%ni, 77.6%id, 0.1%wa, 0.0%hi, 0.3%si, 0.0%st
Cpu10 : 19.5%us, 1.6%sy, 0.0%ni, 78.8%id, 0.1%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu11 : 19.9%us, 2.2%sy, 0.0%ni, 77.5%id, 0.1%wa, 0.0%hi, 0.3%si, 0.0%st
Mem: 32846220k total, 32593588k used, 252632k free, 434464k buffers
Swap: 4194296k total, 0k used, 4194296k free, 22380012k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14489 hadoop 20 0 1643m 684m 16m S 104.7 2.1 32:15.93 java
14496 hadoop 20 0 1635m 705m 16m S 104.7 2.2 32:14.63 java
16194 hadoop 20 0 1637m 655m 16m S 104.7 2.0 29:45.06 java
16243 hadoop 20 0 1630m 687m 16m S 104.7 2.1 29:28.34 java
hoặc
$ mpstat -P ALL
Linux 2.6.32-358.11.1.el6.x86_64 (bb2-dn07) 07/20/2014 _x86_64_ (12 CPU)
08:39:53 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle
08:39:53 PM all 19.88 0.00 2.09 0.23 0.00 0.27 0.00 0.00 77.53
08:39:53 PM 0 20.52 0.00 2.38 0.42 0.00 0.46 0.00 0.00 76.21
08:39:53 PM 1 20.19 0.00 1.85 0.51 0.00 0.11 0.00 0.00 77.35
08:39:53 PM 2 19.95 0.00 1.81 0.48 0.00 0.11 0.00 0.00 77.65
08:39:53 PM 3 19.92 0.00 2.33 0.20 0.00 0.38 0.00 0.00 77.16
08:39:53 PM 4 19.82 0.00 2.28 0.39 0.00 0.45 0.00 0.00 77.07
08:39:53 PM 5 19.74 0.00 2.33 0.19 0.00 0.38 0.00 0.00 77.36
08:39:53 PM 6 20.14 0.00 1.64 0.13 0.00 0.01 0.00 0.00 78.09
08:39:53 PM 7 19.65 0.00 2.19 0.13 0.00 0.35 0.00 0.00 77.69
08:39:53 PM 8 19.38 0.00 2.20 0.11 0.00 0.34 0.00 0.00 77.97
08:39:53 PM 9 19.78 0.00 2.23 0.07 0.00 0.35 0.00 0.00 77.57
08:39:53 PM 10 19.53 0.00 1.59 0.11 0.00 0.01 0.00 0.00 78.76
08:39:53 PM 11 19.93 0.00 2.21 0.07 0.00 0.34 0.00 0.00 77.45
hoặc
$ cat /proc/cpuinfo
.....
Bên cạnh chỉ số tận dụng CPU bạn có thêm 1 chỉ số nữa gọi là “tải trung bình”. Bạn nên hiểu 2 giá trị này thế nào?
Tỉ lệ tận dụng CPU nói rằng một process sử dụng CPU nhiều hay ít. Giả sử bạn có một tính toán khá lớn (ví dụ sắp xếp 10GB dữ liệu), phần lớn thời gian của CPU của bạn chắc chắn sẽ bận rộn so sánh và di chuyển dữ liệu. Phần trăm sử dụng CPU sẽ cao, thời gian rảnh (idle) của CPU chắc sẽ thấp.
Tải trung bình nói rằng số lượng process đang đợi CPU là lớn hay nhỏ. Nếu số lượng process đợi CPU lớn, thời gian một process đợi sẽ dài, thời gian hoàn thành tác vụ của process đó sẽ dài. Bạn sẽ phải chờ kết quả lâu hơn. Ngược lại nếu số lượng process đợi thấp, bạn sẽ không phải đợi các process khác. Thời gian bạn đợi kết quả sẽ chỉ là thời gian tính toán.
Làm thế nào để biết được số lượng process đang đợi CPU là lớn hay nhỏ? Giống như trường hợp trạm thu phí, nếu số lượng process lớn hơn số lượng lõi CPU, chắc chắn sẽ phải có process đợi. Ngược lại nếu số lượng process nhỏ hơn số lượng lõi CPU, các process hầu như sẽ không phải xếp hàng chờ đợi mà sẽ được OS gán cho lõi đang rảnh rỗi tính toán.
Từ đây đặt ra câu hỏi: “đánh giá hiệu năng máy tính dựa vào tỉ lệ sử dụng CPU và tải trung bình như thế nào?”.
Việc đánh giá hiệu năng CPU tùy thuộc vào từng bài toán cụ thể. Ta sẽ đánh giá về hiệu năng sử dụng CPU qua các trường hợp sau (giả sử máy tính có 6 cores 12 threads - ví dụ Intel Xeon):
Trong trường hợp đầu, máy tính của bạn hầu như không dùng CPU mấy. CPU dành hầu hết thời gian cho tính toán thấp, số lượng process cũng không cao. Đứng từ góc độ chi phí, bạn đã chi tiền mua 1 CPu quá tốt so với nhu cầu thực tế :-)
Trường hợp 2, bạn đang sử dụng CPU ở mức khá. Bạn bắt CPU tính toán cật lực. Tuy vậy tải trung bình của CPU chỉ có 3, có nghĩa là năng lực CPU của bạn vẫn còn rất lớn mà bạn hoàn toàn có thể tận dụng. Bạn hoàn toán có thể bật thêm 9 processes với mức tính toán như hiện tại mới có thể tận dụng được hết hiệu năng của CPU.
Trường hợp 3 khá lý thú. CPU của bạn được dùng cho những tính toán rất nhẹ nhàng có thể xong ngay lập tức nhưng số lượng process cần CPU lại khá cao. Điều này nói lên rằng CPU của bạn đang bị quá tải process. Có nhiều lý do dẫn đến trường hợp này và mỗi trường hợp có nhiều cách giải quyết khác nhau. Một ví dụ cho trường hợp này là máy chủ web. Việc render các trang web là tính toán không hề nặng, tuy vậy với các máy chủ web chịu trafic lớn (số lượng connection lớn), các process phục vụ request sẽ phải xếp hàng dẫn đến tình trạng trang web bị phục vụ thời gian kéo dài hơn. Một ví dụ khác là máy chủ dành thời gian chủ yếu đợi thao tác vào ra (I/O) chẳng hạn nhưng truy vấn cơ sở dữ liệu. Số lượng query lớn, số lượng truy vấn cần sắp xếp lớn nhưng dữ liệu cần sắp xếp lại bé, thời gian đợi dữ liệu từ đĩa cứng lại cao. Vì vậy phần lớn CPU sẽ idle, nhưng tải CPU vẫn cao. Đối với trường hợp này, ta chỉ có cách là mua CPU với tần số thấp hơn và chia tải ra nhiều máy hơn để tối ưu hóa chi phí.
Trường hợp 4 là trường hợp bạn đang sử dụng CPU một cách hiệu quả nhất. Mỗi cores đều bận rộn tính toán và hầu hết các cores đều được cho sử dụng. Tùy bài toán tính toán mà trường hợp này có thể là tốt hay xấu. Nếu đây là máy chủ web có lẽ đã đến lúc bạn mua thêm máy tính.
Hiểu được ý nghĩa của tải trung bình, chúng ta hiểu rằng sử dụng CPU hiệu quả có nghĩa là phải overload CPU. Một máy tính với CPU được sử dụng hết công suất suất là một máy tính được sử dụng tốt. Nắm được cách sử dụng vũ khí tải trung bình, chúng ta sẽ thử áp dụng cho 2 trường hợp thực tế.
Bạn có máy tính chuyên trả về file static (css, image, js). Bạn sử dụng nginx và cấu hình để nginx trả về dữ liệu trong một thư mục nhất định. Bạn sẽ cấu hình nginx với bao nhiêu workers.
Trả lời: 12! Nếu bạn cấu hình ít hơn 12 workers, khả năng cao là CPU của máy tính bạn đang không được sử dụng hết công suất. Tại một thời điểm nào đó sẽ có một vì cores rong chơi.
Giả sử bạn có máy chủ web 12 cores (logic :-)) và load average hiện tại là 5. Liệu đã đến lúc bạn mua thêm máy chủ mới chưa?
Trả lời: Không biết :-). Nếu máy chủ của bạn dành phần lớn thời gian idle đợi dữ liệu từ đĩa cứng hoặc cơ sở dữ liệu, nút thắt cổ chai hệ thống của bạn không phải là CPU mà có thể là cơ sở dữ liệu hoặc là đĩa cứng (thao tác I/O). Nếu cơ sở dữ liệu của bạn chưa hết công suất (I/O chưa hết công suất), bạn hoàn toàn không cần mua thêm máy chủ web. Bạn có thể cầu hình lại nginx / gunicorn…) để load average cao hơn (không quá 12 - số lượng cores) nhằm tận dụng hết năng lực của CPU của máy tính hiện tại).
hadoop nổi tiếng trong giới BigData. Một datanode chạy các thủ tục map / reduce viết bằng java để lấy 1 block dữ liệu từ ổ cứng; chạy thao tác map để trích xuất dữ liệu; chạy thao tác reduce để tổng hợp dữ liệu. Một datanode thực hiện rất nhiều truy vấn dữ liệu từ ổ cúng cũng như sử dụng rất nhiều cpu cho thao tác sắp xếp, tổng hợp dữ liệu. Với 1 máy tính 12 cores, bạn sẽ cấu hình bao nhiêu java process cho thao tác map/reduce?
Trả lời: Không biết :-) nhưng chắc chắn là lớn hơn 12. Bạn sẽ bất ngờ vì thấy câu trả lời hơi khác máy chủ web dù rằng bài toán có vẻ giống nhau! Lý do là: mô hình map/reduce của hadoop cần rất nhiều dữ liệu do vậy truy vấn đĩa cứng sẽ rất cao, thao tác I/O lớn. Dù thao tác sắp xếp dữ liệu cũng khá tốn CPU nhưng để có dữ liệu sắp xếp, 1 map process vẫn cần thời gian để chờ dữ liệu từ ổ cúng. Trong khoảng thời gian này CPU sẽ idle. Nếu bạn chỉ cấu hình số lượng map/reduce là 12 (bằng số lượng cores), sẽ có 1 khoảng thời gian mà các cores không làm việc vì phải chờ đĩa cứng. Vì vậy CPU thực chất sẽ có những lúc rất bận và những lúc rất rảnh. Để hạn chế thời gian rảnh của CPU, “best-practice” sẽ là overload CPU bằng cách cấu hình cho số lượng process lớn hơn số cores. Tỉ lệ được khuyến cáo là 1.5 lần. Nhờ vậy trong khi có những process đợi I/O, CPU sẽ bận rộn với các process trước đó.
Cấu hình cụ thể là bài toán tùy trường hợp. Bạn nên xem bản chất bài toán và hành vi của máy chủ trước khi cấu hình
Bài viết đã giải thích ý nghĩa của load-average, một chỉ số quan trọng cũng như giới thiệu một số trường hợp cấu hình thực tế liên quan đến load-average. Hy vọng qua bài viết này, bạn hiểu được ý nghĩa của load-average, áp dụng vào thực tiễn công việc sử dụng máy tính hiệu quả nhất với chi phí tốt nhất.
Một câu hỏi phỏng vấn vị trí SRE của Google:
Lệnh uptime trả về 3 kết quả Load Average. 3 con số này là gì?
Gần đây tôi có đọc cuốn sách tựa đề ‘Practical Object Oriented Design in Ruby’ của Sandi Metz, một diễn giả ưa thích của tôi. Cuốn sách dành phần lớn để nói về các kĩ thuật thiết kế phần mềm với đối tượng là ngôn ngữ là Ruby. Tuy nhiên có rất nhiều ý tưởng của tác giả mà không chỉ giới hạn ở Ruby nói chung mà có thể áp dụng cho bất kì ngôn ngữ nào. Trong bài viết này tôi sẽ liệt kê ra một số ý trong cuốn sách mà tôi thấy rất hay và đáng nhớ.
Hãy bắt đầu từ việc tại sao nên dành thời gian cho việc thiết kế phần mềm:
Vậy việc nên dành thời gian nghiêm chỉnh để thiết kế phần mêm là nên, thì chúng ta nên thiết kế theo phương pháp thế nào:
Khi đã quyết được phương pháp thiết kế, việc quan trọng nhất, khó nhất, đó là bắt tay vào làm, bắt tay vào thiết kế chương trình. Để làm được việc này tốt quả thật là rất khó, bởi vì không có một tiêu chuẩn chung nào có thể áp dụng cho mọi yêu cầu, mọi chương trình. Bản thân tôi cũng là một junior software developer, nên tôi luôn gặp khó khăn mỗi khi viết một chương trình từ đầu (from the scratch). Có rất nhiều cách để giảm khó khăn, và tăng khả năng thiết kế của bạn như: nắm vững về các design pattern, nắm vững về domain logic, đọc về kiến trúc của các phần mêm open source nổi tiếng, và sử dụng các “luật” về thiết kế. Ở dưới đây tôi sẽ nói về một số “luật” mà cuốn sách đề cập đến, mà bản thân tôi thấy khá hữu dụng.
Rule 1: Nền tảng cơ bản của việt thiết kế hướng đối tượng, là việc các đối tượng thao tác với nhau qua việc gửi thông điệp (sending message). Do đó mà việc thiết kế một phần mêm sẽ xoay quanh việc bạn thiết kế sao cho các đối tượng gửi thông điệp cho nhau thông qua một interface dễ hiểu nhất, rõ ràng nhất. Hãy luôn hình dung bài toán của bạn sẽ được giải quyết thông qua một loạt các đối tượng gửi rất nhiều loại thông điệp cho nhau, bạn sẽ hình dung được kiến trúc tổng thể của chương trình dễ dàng hơn.
Rule 2: Single Responsibility: Đây là một luật khá cơ bản trong thiết kế hướng đối tượng. Ai cũng biết về luật này nhưng rất khó để làm theo, nhất là khi khối lượng chương trình tăng lên, và công việc chính của bạn hàng ngày là thêm logic vào một code base đã có. Luật này nói rằng mỗi class chỉ nên đảm trách một vai trò duy nhất. Làm thế nào để đảm bảo tính chất này là một việc khá mơ hồ. Cuốn sách nói rằng với mỗi class, bạn nên có thử mô tả về nó chỉ trong 1 câu. Làm được việc này một cách dễ dàng đảm bảo cho việc logic của class đó thống nhất và không bị lai tạp.
Rule 3: Giảm sự kết dính của code (Writing loosely coupled code). Cá nhân tôi thấy rule này là rule quan trọng bậc nhất trong việc thiết kế phần mềm. Muốn đánh giá một phần mềm được thiết kế tồi hay không, hãy nhìn vào việc các logic có bị kết dính(couple) hay phụ thuộc vào nhau hay không. Vậy các bạn sẽ hỏi “kết dính” cụ thể ở đây có nghĩa là gì? Sự kết dính được hình thành khi logic này “phụ thuộc” vào logic khác. Cụ thể hơn ở khái niệm phụ thuộc, đó là việc mà khi mà một trong logic của class A lại chứa các logic class B, hay nói cách khác là khi A “biết” quá nhiều về B thì khi đó A sẽ phụ thuộc vào B. Khái niệm này hay được nhắc đến bằng những cụm từ khác như là logic hiding, tức khi thiết kế một class, bạn phải giấu logic của class đó càng nhiều càng tốt. Đó chính là lý do tại sao các ngôn ngữ như java có những keyword như public, private hay protected. Vậy quay lại từ đầu, để giảm sự kết dính của code thì chúng ta phải làm một việc là thiết kế sao cho các class không phụ thuộc vào nhau, và “biết” càng ít về nhau càng tốt. Vì vậy mỗi khi bạn viết một đoạn code nào đó, bạn hãy tự đọc lại và xem đoạn code đó có sử dụng quá nhiều logic của một class hay logic bên ngoài không. Để giải quyết cho việc “writing loosely coupled code” thì có khá nhiều kĩ thuật nổi tiếng như là: Inject Dependencies, Isolate Dependencies, Reversing Dependencies mà nếu có dịp tôi sẽ giới thiệu trong một bài viết khác. Ngoài ra còn có một luật rất hữu dụng để giải quyết vấn đề kết dính của code được gọi là Law of Demeter, các bạn có thể tham khảo ở đường link tôi vừa gửi.
Bản thân việc testing không nằm trong khâu “thiết kế” phần mềm. Auto testing (hay là unit test) chỉ là một bước để đảm bảo rằng logic hiện tại đang có là đang chạy “gần như là” tốt (nói gần như là do unit test không thể đảm bảo 100% việc “chạy tốt” của tất cả mọi logic. Tuy nhiên chỉ việc đảm bảo “gần như” tốt thôi đã cho thấy tầm quan trọng của testing. Có một việc mà bất kì một nhà phát triển nào khi mới bắt đầu viết test, và ngay cả những người đã quá quen việc kĩ thuật TDD (Test Driven Development) cũng sẽ băn khoăn, đó là việc nên test cái gì. Trong cuốn sách Sandi đã chia khá rõ ràng về 2 phần mà bạn nên test đó là:
Như tôi đã đề cập ở trên, bản chất của việc thiết kế hướng đối tượng xoay quanh việc các class sẽ gửi message cho nhau. Do đó khi test chúng ta cũng nên xoay quanh khái niệm mesage này. Một cách đơn giản, Incomming Message tức là các message được “gửi” đến một object X, và test các message đó tương đương với việc bạn sẽ test các interface của object X đó được công khai (public interface) ra ngoài. Outgoing Message hơi phức tạp hơn một chút, giả sử bạn có một object X với method Foo, trong Foo sẽ gọi method Bar để thực hiện một logic nào đó. Việc test Foo sẽ gọi Bar đúng N lần, với kết quả nhất định sẽ được gọi là test Outgoing Message. Việc chia ra làm 2 loại message cần test sẽ giúp cho bạn nhìn thấy một cách rõ ràng hơn cái gì nên test, và cái gì không nên test.
Ở trên tôi đã trình bày về một số suy nghĩ của cá nhân, và các suy nghĩ của Sandi Metz trong cuốn sách về thiết kế phần mềm. Bản thân việc thiết kế được phần mềm tốt là rất khó, mà mỗi một dạng phần mềm, với mỗi một logic domain lại có một cách giải quyết riêng. Không có một cách giải quyết nào chung cho mọi bài toán cả, nhưng có một số qui tắc chung mà bán có thể áp dụng được cho nhiều bài toán khác nhau. Để nắm được các qui tắc đó đòi hỏi bạn không những phải đọc nhiều, làm nhiều, tích luỹ nhiều kinh nghiệm, mà còn dựa trên việc bạn thất bại nhiều nữa. Tạo ra các phần mềm tồi, khó bảo trì cũng là một bước đệm tốt để bạn rút kinh nghiệm cho các lần sau :).
]]>Có 1 người bạn gần đây bắt đầu lập trình với threads và thiết kế chương trình như sau.
Chương trình có đầu vào là một mảng gồm một số phần tử (khoảng vài chục). Chương trình làm nhiệm vụ duyệt từng phần tử trong mảng, tính toán và trả về kết quả đối với từng phần tử. Bạn mình thiết kế chương trình bằng cách với mỗi phần tử trong mảng, bạn tạo một thread và cho thread thực hiện tính toán với phần tử đó.
Khi mình hỏi tại sao bạn lại thiết kế chương trình như thế thì bạn trả lời: các thread sẽ chạy song song nên về lý thuyết càng nhiều thread thì chương trình chạy càng nhanh!
Mình nhận ra bạn mình có về không biết định luật Amdahl, tuy đơn giản nhưng lại là một định luật rát quan trọng trong tính toán song song. Khi hiểu định luật này chắc chắn bạn sẽ có cái nhìn tổng quan hơn về hệ thống máy tính nói chung, và cụ thể là lập trình multithread nói riêng. Trong bài viết này, mình muốn giới thiệu định luật Amdahl.
Giả sử bạn thay CPU mới có tốc độ cao hơn CPU cũ.
Định luật Amdahl nói rằng sự tằng tốc nhờ cải thiện hiệu năng của CPU = thời gian chạy toàn bộ tác vụ khi sử dụng CPU cũ / thời gian chạy toàn bộ tác vụ khi sử dụng CPU mới.
Độ tăng tốc phụ thuộc vào 2 thừa số:
Thời gian chạy với CPU mới = Thời gian chạy CPU cũ * (1 - tỉ lệ chương trình có thể cải thiện nhờ CPU mới + tỉ lệ chương trình có thể cải thiện nhờ CPU mới / độc tăng tốc thu được từ CPU mới).
Độ tăng tốc tổng thể = Thời gian chạy trên CPU cũ / Thời gian chạy trên CPU mới
= 1 / (1 - tỉ lệ chương trình cải thiện nhờ CPU mới + tỉ lệ chương trình cải thiện nhờ CPU mới / độ tăng tốc thu được từ CPU mới)
Bạn thay CPU cho máy chủ web. CPU mới chạy nhanh hơn CPU cũ 10 lần. Chương trình web của bạn giả sử tốn 60% cho SQL (I/O) và 40% tính toán (nhận kết quả từ cơ sở dữ liệu, render page). Hỏi tốc độ cải thiện từ việc thay CPU là bao nhiêu?
Giải:
Độ tăng tốc tổng thể = 1 / (0.6 + 0.4/10) = 1 / 0.64 = 1.56
Vậy dù rằng CPU có tính nhanh 10 lần thì tốc độ của cả hệ thống chỉ được cải thiện 1.56 lần.
Hàm căn bậc hai của một số thực được sử dụng rất nhiều trong đồ hoạ máy tính. Giả sử tính toán căn bậc 2 chiếm 20% tổng thời chạy của thao tác đồ hoạ. Bạn muốn tăng tốc độ của hệ thống đồ hoạ của bạn. Có 2 lựa chọn sau đây:
Bạn sẽ đầu tư tiền hay bỏ thời gian và trí não cải thiện các thao tác còn lại.
Giải:
Trường hợp 1, độ tăng tốc = 1 / (0.8 + 0.2 / 10) = 1 / 0.82 = 1.22
Trường hợp 2, độ tăng tốc = 1 / (0.5 + 0.5 / 1.6) = 1.23
Như vậy lựa chọn 2 cho kết quả tốt hơn 1 chút!
Nếu thử quan sát, bạn sẽ thấy từ công thức Amdahl có thể rút ra là độ tăng tốc phụ thuộc cả vào bản chất bài toán. Nếu tỉ lệ có thể tăng tốc được không cao, việc bạn thêm song song cũng không giải quyết vấn đề gì. Nói cách khác nếu tỉ lệ cải thiện nhờ CPU mới = 0 thì độ tăng tốc tổng thể sẽ là 1 / (1 + 0/10) = 1 tức không thay đổi.
Quay trở lợi vấn đề của bạn mình, tại sao mình lại nghĩ việc tăng số thread lên không giải quyết được tốc độ?
Giả sử bạn CPU bạn có 4 cores (Ví dụ Corei7 MQ). Chương trình của bạn sẽ được lập lịch bởi kernel. Nếu bạn dùng 2 threads, tại thời điểm CPU được cấp cho process của bạn, 2 cores sẽ được sử dụng để chạy chương trình. Giả sử chương trình bạn dùng CPU để tính toán 50% thời gian, 50% thời gian còn lại được chia đều cho các cores.
Nếu không dùng thread, chương trình của bạn là 1 chương trình liên tục bình thường, tốc độ sẽ cải thiện sẽ là:
Độ tăng tốc = 1 / (0.5 + 0.5 / 1) = 1 (không tăng tí nào!)
Nếu bạn dùng 2 threads:
Độ tăng tốc = 1 / (0.5 + 0.5 / 2) = 1.33 (Tăng 33%!)
Nếu bạn dùng 4 threads:
Độ tăng tốc = 1 / (0.5 + 0.5 / 4) = 1.6 (Tăng 60%!)
Nếu dùng 8 threads, bạn mong chờ tốc độ tăng tốc là 1.7! Sai lầm! Lý do: giống như quan sát ở trên, bản thân việc chia việc cho CPU không phải là việc làm song song được. Nói cách khác CPU chỉ thực hiện được cùng 1 lúc 4 tác vụ. Nếu có nhiều hơn 4 tác vụ, tỉ lệ thực hiện song song (số task thực hiện đồng thời không đổi, nhưng só task phải thực hiện tăng lên) sẽ giảm khiến hiệu năng toàn hệ thống giảm xuống.
Ví dụ ta có 4 threads, thì số task có thể tận dụng được CPU là 100%. Khi ta có 5 threads, số threads có thể tận dụng được CPU sẽ giảm xuống 80%. Ta có thể xem sự thay đổi về hiệu năng so sánh tương đối với trường hợp 1 thread như sau:
4 threads: Độ tăng tốc = 1 / (0 + 1 / 4) = 4
5 threads: Độ tăng tốc = 1 / (0.2 + 0.8 / 4) = 1 / 0.4 = 2.5
Như vậy độ tăng tốc tương đối với trường hợp chỉ sử dụng 1 thread đã giảm từ 4 lần xuông còn 2.5 lần.
Nói cách khác, khi tất cả các cores đã làm việc thì việc tăng threads sẽ chỉ làm tăng thêm phần không thể tính song song, khiến hiệu năng hệ thống giảm. Ngoài ra còn có các chi phí khác mà ta chưa kể đến như: tạo một thread cũng tốn thời gian, bộ nhớ v.v. Nói cách khác việc tăng thread không làm tăng tốc độ chương trình mà nhiều trường hợp còn làm giảm tốc độ chạy. Suy nghĩ lúc đầu của bạn mình là sai lầm!
Như ở ví dụ 2 ở trên, bạn thấy rằng việc mua card đồ hoạ mới không làm tăng hiệu năng tổng thể như việc tối ưu chương trình. Như vậy ta hoàn toàn có thể thay đổi thiết kế chương trình để làm tăng hiệu năng. Ta xét bài toán ví dụ sau đây:
Nhập n. In ra tất cả các số nhỏ hơn n mà là số nguyên tố.
Dưới góc độ thread, ta có 2 cách design hệ thống (Giả định hệ thống có CPU 4 cores)
Bạn sẽ chọn cách nào?
Thoạt nhìn có vẻ 2 cách không có gì khác nhau, nhưng nếu để ý sẽ nhận ra là mật độ số nguyên tố không giống nhau. Nói cách khác nếu làm theo cách 1, sẽ có thread rất nhanh hoàn thành (thread phải xử lý vùng ít số nguyên tố), và có những thread phải làm việc rất vất vả (thread phải xử lý vùng có nhiều số nguyên tố). Nói cách khác cách design 1 có tỉ lệ tính toán có thể cải thiện không cao.
Cách 2 thoạt nhìn có vẻ chậm nhưng lại là cách cho tỉ lệ xử lý song song cao hơn, vì việc xử lý từng số một không phụ thuộc và phân bố của số nguyên tố!
Vậy ta nên thiết kế chương trình theo cách 2!
Bài viết giới thiệu định luật Amdahl, làm rõ ý nghĩa định luật cũng qua 2 ví dụ đồng thời áp dụng định luật Amdahl vào việc thiết kế bài toán đơn giản. Hy vọng qua bài viết bạn hiểu phần nào về đột tăng tốc trong tính toán song song, cũng như biết cách tính toán định lượng để đánh giá các thiết kế (Nhiều khi mua máy mới không hắn đã là tốt!).
Trong loạt bài viết trước về Full-text search, mình đã giới thiệu về các khái niệm hết sức cơ bản để làm nên một search-engine:
Trong bài viết này, để khép lại loạt bài về Full-Text search, mình sẽ hướng dẫn cách làm một search engine hết sức đơn giản sử dụng inverted index. Sample code mình sẽ sử dụng Python để cho dễ hiểu.
Việc đầu tiên trước khi code chúng ta phải design xem chương trình của chúng ta sẽ gồm những module nào, nhiệm vụ mỗi module ra sao. Để design được thì chúng ta phải làm rõ yêu cầu bài toán và cách giải quyết.
Bài toán trong bài viết này là xây dụng một search engine. Để cho đơn giản chúng ta sẽ xây dựng search engine trên command line, dựa trên đầu vào là các documents với format được định nghĩa trước. Trong bài này, mình sẽ sử dụng một sample nhỏ của twitter data như là input documents. Bài toán được tóm tắt lại như dưới đây:
1 2 3 4 5 6 |
|
Như đã giới thiệu ở loạt bài trước, Full Text Search sử dụng inverted index để lưu lại term và các document chứa term đó:
1
|
|
Vì vậy chúng ta cần một module để lưu Structure này, chúng ta sẽ gọi module đó là DocID. DocID sẽ có nhiệm vụ là lưu term và một array để chứa id của các documents mà chứa term đó.
Tuy nhiên chỉ lưu đơn thuần dữ liệu inverted index thì sẽ không đủ, chúng ta cần một module để lưu lại các document và ID của chúng để khi có kết quả tìm kiếm chúng ta có thể present kết quả dễ dàng hơn. Module này chúng ta sẽ gọi là Content. Content sẽ lưu lại Id và nội dung của document.
Chúng ta cũng sẽ cần một module để làm nhiệm vụ phân tích document ra thành các term như đã giới thiệu trong Bài 2: Kỹ thuật Tokenize. Chúng ta sẽ gọi module này là Tokenizer.
Đã có Tokenizer, DocID, Content, chúng ta cần một module sử dụng cả 3 module này để lưu trữ thông tin được tạo ra từ Tokenizer vào DocID và Content, chúng ta sẽ gọi nó là Indexer.
Cuối cùng, chúng ta cần một module sử dụng boolean logic như đã giới thiệu trong Bài 3 để tìm kiếm. Chúng ta sẽ gọi module này là Searcher. Module Searcher sẽ có nhiệm vụ sử dụng tách query ra thành các term, search ra một tập document chứa các term đó, và present ra màn hình.
Tóm tắt lại chúng ta sẽ có các module sau:
Để implement bài toán này chúng ta sẽ sử dụng python. Chúng ta cũng cần một thư viện để lưu lại/sử dụng (dump) dữ liệu (inverted index) ra file. Python có một thư viện rất tốt để dump data structure ra file gọi là Pickle.
Sử dụng pickle, chúng ta sẽ lưu dữ liệu ra file và khi load chương trình lên sẽ sử load file vào data structure lên sau. Tách ra làm 2 bước như vậy giúp chúng ta tách biệt được 2 quá trình 1) Index và 2) Search, mà qua đó khi có thêm dữ liệu mới, index file sẽ được update thêm mà không ảnh hưởng đên Searcher.
Dưới đây chúng ta sẽ đi lần lượt vào implementation của từng module. Đầu tiên là DocID.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Ở DocID chúng ta có property docIDTable được lưu dưới dạng dictionary của python, mà trong đó key là term , và value sẽ là array chứa Ids của các document chứa term đó. docIDTable chính là biểu diễn bằng code của inverted index data structure.
Module DocID có các hàm dump và load để lưu dữ liệu ra file và load dữ liệu lên memory. Tiếp theo chúng ta sẽ đến với implemetation của module Content.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Module này chỉ nhằm nhiệm vụ lưu lại dữ liệu của document và Id của document đó. Việc này sẽ được thực hiện cũng qua dictionary của python, với key là Id của document, và value là content của document tương ứng.
Tiếp đến, module Tokenizer sẽ được implement như dưới đây
Để cho đơn giản, tokenizer của chúng ta sẽ sử dụng ngram.
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
Module này có nhiệm vụ là sử dụng Tokenizer để tách input document thành các term sử dụng ngram, tức là mỗi term sẽ có độ dài bằng độ dài ngram. Sau đó sẽ index term đó vào DocID, nếu term đó đã tồn tại thì id của document hiện tại sẽ được add vào docIDTable của docId thông qua hàm “set”.
Kết quả index sẽ được lưu vào file docid.pickle (inverted index data) và content.pickle (content data).
Module này có nhiệm vụ load dữ liệu đã qua index từ 2 file docid.pickle và content.pickle vào memory, sau đó với mỗi query, Searcher sẽ phân tích query đó thành các term dựa vào tokenizer, tìm kiếm document chứa các term đó dựa vào dữ liệu từ docid, và present kết quả ra màn hình dựa vào dữ liệu lấy được từ content.pickle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Chúng ta có thể thấy Searcher là một module rất đơn giản sử dụng tokenizer để bóc tách query. Sau khi bóc tách query thành các term, với mỗi term chúng ta sẽ tìm các document chứa term đó dựa vào docID. Mỗi term chúng ta sẽ thu được một chuỗi Ids chứa id của document chứa chúng.
Để kết hợp các các chuỗi ids tìm được thành kết quả cuối cùng, chúng ta làm một mô hình ranking rất đơn giản, document nào chứa nhiều term hơn thì hiển thị trước. Logic này được thực hiện dựa vào tạo một dictionary chứa kết quả (self.result) , cứ mỗi khi tìm được document nào thì ta cộng kết quả thêm 1.
Kết quả cuối cùng sẽ được in ra màn hình thông qua hàm print_result. Như vậy chúng ta đã implement xong một search engine hết sức đơn giản.
Thông qua chuỗi bài viết, chúng ta đã hiểu được phần nào việc tạo ra một search engine. Để có một search engine thành công, như google hay yahoo, không những performance phải được hoàn thiện ở mức tối đa với khối lượng dữ liệu rất lớn, thì việc có một mô hình ranking thích hợp cũng vô cùng quan trọng. Hy vọng chuỗi bài viết đã đem đến cho các bạn cái nhìn cơ bản nhất về search engine.
]]>Quy hoạch động là một trong những kĩ thuật lập trình cơ bản được sử dụng khá nhiều trong các cuộc thi lập trình. Ý tưởng về cơ bản rất đơn giản: để giải một bài toán, chúng ta đi giải các bài toán con, sau đó tổng hợp các lời giải đó lại thành lời giải của bài toán ban đầu. Trong một số bài toán, nếu không sử dụng quy hoạch động, rất nhiều bài toán con sẽ bị tính lặp đi lặp lại. Quy hoạch động sẽ tìm cách để giải mỗi bài toán con đúng 1 lần để giảm thiểu số lần tính toán. Một khi lời giải cho một bài toán con đã có, chúng ta lưu lại và lần tiếp theo cần lời giải đó, chúng ta chỉ cần tìm lại.
Quy hoạch động được sử dụng rất nhiều trong các thuật toán khác, ví dụ như: thuật toán Dijkstra tìm đường đi ngắn nhất, Knapsack, Nhân ma trận theo chuỗi (Chain matrix multiplication), thuật toán Floyd-Warshall tìm đường đi ngắn nhất giữa mọi cặp đỉnh trong đồ thị (đã có bài viết giới thiệu về thuật toán này).
Trong bài viết này, chúng ta sẽ cùng đi qua một số ví dụ sử dụng quy hoạch động trên TopCoder.
A sequence of numbers is called a zig-zag sequence if the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative. A sequence with fewer than two elements is trivially a zig-zag sequence.
For example, 1,7,4,9,2,5 is a zig-zag sequence because the differences (6,-3,5,-7,3) are alternately positive and negative. In contrast, 1,4,7,2,5 and 1,7,4,5,5 are not zig-zag sequences, the first because its first two differences are positive and the second because its last difference is zero.
Given a sequence of integers, sequence, return the length of the longest subsequence of sequence that is a zig-zag sequence. A subsequence is obtained by deleting some number of elements (possibly zero) from the original sequence, leaving the remaining elements in their original order.
{ 1, 7, 4, 9, 2, 5 }
Returns: 6
The entire sequence is a zig-zag sequence.
{ 1, 17, 5, 10, 13, 15, 10, 5, 16, 8 }
Returns: 7
There are several subsequences that achieve this length. One is 1,17,10,13,10,16,8.
{ 44 }
Returns: 1
{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }
Returns: 2
{ 70, 55, 13, 2, 99, 2, 80, 80, 80, 80, 100, 19, 7, 5, 5, 5, 1000, 32, 32 }
Returns: 8
{ 374, 40, 854, 203, 203, 156, 362, 279, 812, 955, 600, 947, 978, 46, 100, 953, 670, 862, 568, 188, 67, 669, 810, 704, 52, 861, 49, 640, 370, 908, 477, 245, 413, 109, 659, 401, 483, 308, 609, 120, 249, 22, 176, 279, 23, 22, 617, 462, 459, 244 }
Returns: 36
Bài toán này là một dạng của bài toán tìm xâu dài nhất thoả mãn một điều kiện nào đó, ví dụ như tăng dần, giảm dần… Cách làm quy hoạch động là như sau: duyệt từ trái sang phải, tìm xâu dài nhất kết thúc tại phần tử đang xét. Xâu dài nhất này được tính dựa trên các bài toán con phía trước:
Xem có thể thêm phần tử hiện tại vào các xâu dài nhất két thúc bằng các phần tử phía trước không.
Chọn xâu dài nhất có thể trong các xâu thoả mãn.
Sau đây là đoạn code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
In the city, roads are arranged in a grid pattern. Each point on the grid represents a corner where two blocks meet. The points are connected by line segments which represent the various street blocks. Using the cartesian coordinate system, we can assign a pair of integers to each corner as shown below.
You are standing at the corner with coordinates 0,0. Your destination is at corner width,height. You will return the number of distinct paths that lead to your destination. Each path must use exactly width+height blocks. In addition, the city has declared certain street blocks untraversable. These blocks may not be a part of any path. You will be given a String[] bad describing which blocks are bad. If (quotes for clarity) “a b c d” is an element of bad, it means the block from corner a,b to corner c,d is untraversable. For example, let’s say width = 6 length = 6 bad = {“0 0 0 1”,“6 6 5 6”} The picture below shows the grid, with untraversable blocks darkened in black. A sample path has been highlighted in red.
Examples
6
6
{“0 0 0 1”,“6 6 5 6”}
Returns: 252
Example from above.
1
1
{}
Returns: 2
Four blocks aranged in a square. Only 2 paths allowed.
35
31
{}
Returns: 6406484391866534976
Big number.
2
2
{“0 0 1 0”, “1 2 2 2”, “1 1 2 1”}
Returns: 0
Vẫn trên tư tưởng quy hoạch động, dễ thấy ta cần duyệt từ đỉnh (0,0). Số lượng đường đi đến đỉnh (i,j) sẽ dựa trên số lượng đường đi đến đỉnh (i-1,j) và đỉnh (i, j-1). Chú ý nếu đường đi từ (i-1,j) hoặc (i, j-1) đến (i,j) bị chặn thì ta sẽ không tính đoạn đường đó nữa.
Sau đây là đoạn code (C++):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
Hi vọng qua 2 ví dụ trên, bạn đã phần nào có được tư tưởng quy hoạch động. Về cơ bản, chúng ta chỉ cần đi đến được cách tính bài toán hiện tại dựa vào các bài toán con trước đó là 90% công việc đã xong. Hãy luyện tập thêm để chiến đấu tại TopCoder!
Khi nhắc đến các thuật toán duyệt đồ thị, có thể bạn đã biết (và đã từng implement) Depth-First Search, Breadth-First Search, hoặc Dijkstra. Xin nhắc lại về ý nghĩa của từng thuật toán, đứng ở khía cạnh bài toán tìm đường đi ngắn nhất. DFS dùng để giải các bài toán mà chúng ta muốn tìm được lời giải (không nhất thiết phải là quãng đường ngắn nhất), hoặc ta muốn thăm tất cả các đỉnh của đồ thị. BFS cũng để duyệt các đỉnh của đồ thị, nhưng có một tính chất quan trọng là: nếu tất cả các cạnh không có trọng số, lần đầu tiên một đỉnh được thăm, ta có ngay đường đi ngắn nhất đến đỉnh đó. Bây giờ đến thuật toán Disjkstra, đây là thuật toán nổi tiếng dùng để tìm đường đi ngắn nhất từ một đỉnh cho trước đến các đỉnh còn lại, trong một đồ thị có các cạnh có trọng số không âm. Như vậy, Dijkstra đã tiến hơn một bước so với BFS.
Đó là sơ qua về ba thuật toán mà có thể mọi người đều đã biết. Trong bài viết này, tôi xin giới thiệu một thuật toán ít biết đến hơn để duyệt đồ thị, đó là Floyd-Warshall.
Nếu như Dijkstra giải quyết bài toán tìm đường đi ngắn nhất từ một đỉnh cho trước đến mọi đỉnh khác trong đồ thị, thì Floyd-Warshall sẽ tìm đường đi ngắn nhất giữa mọi đỉnh sau một lần chạy thuật toán. Một tính chất nữa là Floyd-Warshall có thể chạy trên đồ thị có các cạnh có trọng số có thể âm, tức là không bị giới hạn như Dijkstra. Tuy nhiên, lưu ý là trong đồ thị không được có vòng (cycle) nào có tổng các cạnh là âm, nếu có vòng như vậy ta không thể tìm được đường đi ngắn nhất (mỗi lần đi qua vòng này độ dài quãng đường lại giảm, nên ta có thể đi vô hạn lần)
Thuật toán Floyd-Warshall so sánh tất cả các đường đi có thể giữa từng cặp đỉnh. Nó là một dạng của quy hoạch động (Dynamic Programming). Đặt hàm adj(i,j,k) là đường đi ngắn nhất từ i đến j, chỉ dùng các đỉnh trong tập {1,2,…,k}. Giả sử ta muốn tính adj{i,j,k+1}. Với mỗi cặp đỉnh i và j, đường đi ngắn nhất có thể là: (1) đường đi chỉ sử dụng các đỉnh trong tập {1,…k} hoặc (2) đường đi từ i đến k+1 rồi từ k+1 đến j, cũng chỉ sử dụng các đỉnh trong tập {1,…k}. Do vậy:
Trường hợp cơ bản: adj(i,j,0) = w(i,j)
Đệ quy: adj(i,j,k+1) = min{adj(i,j,k), adj(i,k+1, k) + adj(k+1, j, k)}
Đây là đoạn pseudocode của Floyd-Warshall (có một chút thay đổi, nhưng ý tưởng là như nhau)
1 2 3 4 |
|
Dễ thấy độ phức tạp thuật toán là O(n^3) với n là số đỉnh của đồ thị.
Tư tưởng của thuật toán Floyd-Warshall trong việc tìm đường đi ngắn nhất có thể áp dụng vào các bài toán dạng tìm tính chất kết nối giữa các đỉnh. Tôi xin lấy một ví dụ, đó là bài TopCoder SRM 184, Div 2, 1000-point problem
Đề bài như sau (xin chịu khó đọc hiểu đề bài)
You are arranging a weird game for a team building exercise. In this game there are certain locations that people can stand at, and from each location there are paths that lead to other locations, but there are not necessarily paths that lead directly back. You have everything set up, but you need to know two important numbers. There might be some locations from which every other location can be reached. There might also be locations that can be reached from every other location. You need to know how many of each of these there are.
Create a class TeamBuilder with a method specialLocations that takes a String[] paths that describes the way the locations have been connected, and returns a int[] with exactly two elements, the first one is the number of locations that can reach all other locations, and the second one is the number of locations that are reachable by all other locations. Each element of paths will be a String containing as many characters as there are elements in paths. The i-th element of paths (beginning with the 0-th element) will contain a ‘1’ (all quotes are for clarity only) in position j if there is a path that leads directly from i to j, and a ‘0’ if there is not a path that leads directly from i to j.
Examples
{“010”,“000”,“110”} Returns: { 1, 1 } Locations 0 and 2 can both reach location 1, and location 2 can reach both of the other locations, so we return {1,1}.
{“0010”,“1000”,“1100”,“1000”} Returns: { 1, 3 } Only location 3 is able to reach all of the other locations, but it must take more than one path to reach locations 1 and 2. Locations 0, 1, and 2 are reachable by all other locations. The method returns {1,3}.
{“01000”,“00100”,“00010”,“00001”,“10000”} Returns: { 5, 5 } Each location can reach one other, and the last one can reach the first, so all of them can reach all of the others.
{“0110000”,“1000100”,“0000001”,“0010000”,“0110000”,“1000010”,“0001000”} Returns: { 1, 3 }
Solution Về cơ bản, bài này cần tìm số lượng các đỉnh mà từ đó có thể đi đến tất cả các đỉnh khác, và số lượng các đỉnh mà các đỉnh khác đều có thể đi tới. Một ví dụ rất tốt để áp dụng thuật toán Floyd-Warshall tìm tính chất kết nối giữa 2 đỉnh bất kì.
Trong bài này, chúng ta chỉ cần phải kiểm tra xem có đường đi từ đỉnh i đến đỉnh j trong đồ thị hay không. Chúng ta sẽ áp dụng thuật toán Floyd-Warshall trên, nhưng có thay đổi một chút trong dòng xử lý bên trong vòng lặp. Về cơ bản, chúng ta vẫn sử dụng ý tưởng là update thông tin giữa 2 đỉnh i và j, mỗi khi ta có thêm thông tin giữa đỉnh i và đỉnh k, đỉnh k và đỉnh j, với k là một đỉnh khác i và j. Nhưng ta không cập nhật thông tin về * đường đi ngắn nhất* nữa, mà ta cập nhật thông tin về có hay không đường đi từ i đến j. Với mỗi cặp đỉnh i và j chưa có kết nối, ta sẽ kiểm tra xem nếu có đường đi từ i đến k và từ k đến j, thì ta cập nhật là có đường đi từ i đến j.
Sau đây là đoạn code C++ minh hoạ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
|
Trong phần 3, các bạn đã được tìm hiểu về việc sử dụng Boolean Logic để tìm ra các Document chứa các term trong query cần tìm kiếm. Vậy sau khi tìm được các Document thích hợp rồi thì chỉ việc trả lại cho người dùng, hay đưa lên màn hình? Bài toán sẽ rất đơn giản khi chỉ có 5, 10 kết quả, nhưng khi kết quả lên đến hàng trăm nghìn, thì mọi việc sẽ không đơn giản là trả lại kết quả nữa. Lúc đó sẽ có vấn đề mới cần giải quyết, đó là đưa kết quả nào lên trước, hay chính là bài toán về Ranking
Việc Ranking trong Full Text Search thông thường sẽ được thực hiện thông qua việc tính điểm các Document được tìm thấy, rồi Rank dựa vào điểm số tính được. Việc tính điểm thế nào sẽ được thực hiện thông qua các công thức, hay thuật toán, mà mình gọi chung là Ranking Model
Trong bài viết về Ranking news, mình đã nói về việc giải quyết một bài toán gần tương tự. Tuy nhiên bài toán lần này cần giải quyết khác một chút, đó là việc Ranking sẽ phải thực hiện dựa trên mối quan hệ giữa “query terms” và “document”.
Ranking Model được chia làm 3 loại chính: Static, Dynamic, Machine Learning. Dưới đây mình sẽ giới thiệu lần lượt về mỗi loại này.
Static ở đây có nghĩa là, Ranking Model thuộc loại này sẽ không phụ thuộc vào mối quan hệ ngữ nghĩa giữa “query term” và “document”. Tại sao không phụ thuộc vào “query term” mà vẫn ranking được? Việc này được giải thích dựa theo quan điểm khoa học là độ quan trọng của document phụ thuộc vào mối quan hệ giữa các document với nhau
.
Chúng ta sẽ đi vào cụ thể một Ranking Model rất nổi tiếng trong loại này, đó chính là PageRank. PageRank là thuật toán đời đầu của Google, sử dụng chủ yếu cho web page, khi mà chúng có thể “link” được đến nhau. Idea của PageRank là “Page nào càng được nhiều link tới, và được link tới bởi các page càng quan trọng, thì score càng cao”. Để tính toán được PageRank, thì chúng ta chỉ cần sử dụng WebCrawler để crawl được mối quan hệ “link” giữa tất cả các trang web, và tạo được một Directed Graph của chúng.
Chính vì cách tính theo kiểu, tạo được Graph xong là có score, nên mô hình dạng này được gọi là “Static”.
Ngoài PageRank ra còn có một số thuật toán khác gần tương tự như HITS đã từng được sử dụng trong Yahoo! trong thời gian đầu.
Ranking Model thuộc dạng Dynamic dựa chủ yếu vào Mối quan hệ giữa “query term” và “document”. Có rất nhiều thuật toán thuộc dạng này, có thuật toán dựa vào tần suất xuất hiện của “query term” trong document, có thuật toán lại dựa vào các đặc tính ngữ nghĩa (semantic) của query term , có thuật toán lại sử dụng những quan sát mang tính con người như thứ tự xuất hiện các từ trong “query term” và thứ tự xuất hiện trong “document”.
Một trong những thuật toán được sử dụng nhiều nhất là TF-IDF (Term Frequency Inverse Document Frequency). Thuật toán này dựa vào Idea là “query term” xuất hiện càng nhiều trong document, document sẽ có điểm càng cao.
Thuật toán này được biểu diễn dưới công thức sau \[TF-IDF(t, d, D) = TF(t, d) * IDF (t, D)\] Ở đây t là query term, d là document cần được score, và D là tập hợp “tất cả” các documents. Trong đó thì: \[TF(t, d) = frequency(t, d)\] \[IDF(t, D) = log{N \over \|\{d \in D : t \in d\}\|}\]
Một cách đơn giản thì:
Công thức của TF-IDF đã phối hợp một cách rất hợp lý giữa tần suất của term và ý nghĩa/độ quan trọng của term đó.
Trong thực tế thì người ta hay sử dụng thuật toán Okapi BM25 hay gọi tắt là BM25, là một mở rộng của TF-IDF, nhưng thêm một vài weight factor hợp lý.
Ngoài việc sử dụng các mối quan hệ đơn giản giừa query term và document, hay giứa document với nhau, thì gần đây việc sử dụng học máy (Machine Learning) trong Ranking cũng đang trở nên rất phổ biến. Để nói về Machine Learning thì không gian bài viết này có lẽ là không đủ, mình sẽ nói về ý tưởng của Model này.
Idea của việc sử dụng Machine Learning trong ranking là chúng ta sẽ sử dụng một mô hình xác suất để tính toán. Cụ thể hơn là chúng ta sẽ sử dụng supervised learning, nghĩa là chúng ta sẽ có input là một tập dữ liệu X để training, một model M ban đầu, một hàm error để so sánh kết quả output X’ có được từ việc áp dụng model M vào query term, và một hàm boost để từ kêt quả của hàm error chúng ta có thể tính lại được model M. Việc này được lặp đi lặp lại mỗi lần có query, hoặc lặp lại một cách định kỳ (1 ngày 1 lần, 1 tháng 1 lần..) để model M luôn luôn được cải thiện.
Thuật toán gần đây được sử dụng khá nhiều trong Ranking model chính là Gradient Boosting Decision Tree mà các bạn có thể tham khảo ở đây
Bài viết đã giới thiệu về 3 mô hình chính dùng để Ranking kết quả tìm kiếm trong Full Text Search. Trong thực tế thì các công ty lớn nhưn Google, Yahoo, MS sẽ không có một mô hình cố định nào cả, mà sẽ dựa trên các kết quả có từ người dùng để liên tục cải thiện. Không có một mô hinh nào là “đúng” hay “không đúng” cả, mà để đánh giá Ranking Model chúng ta sẽ phải dựa trên thông kê người dùng (như click rate, view time…). Việc hiểu rõ Ranking Model sẽ giúp chúng ta build được một search engine tốt cho service của mình, đông thời cũng giúp ích rất nhiều cho việc SEO (Search Engine Optimization).
Tài liệu tham khảo: - Yahoo! Learning to Rank Challenge Overview
]]>